IronAlpine.Framework.Security
3.1.1
dotnet add package IronAlpine.Framework.Security --version 3.1.1
NuGet\Install-Package IronAlpine.Framework.Security -Version 3.1.1
<PackageReference Include="IronAlpine.Framework.Security" Version="3.1.1" />
<PackageVersion Include="IronAlpine.Framework.Security" Version="3.1.1" />
<PackageReference Include="IronAlpine.Framework.Security" />
paket add IronAlpine.Framework.Security --version 3.1.1
#r "nuget: IronAlpine.Framework.Security, 3.1.1"
#:package IronAlpine.Framework.Security@3.1.1
#addin nuget:?package=IronAlpine.Framework.Security&version=3.1.1
#tool nuget:?package=IronAlpine.Framework.Security&version=3.1.1
IronAlpine.Framework.Security
JWT bearer authentication, a claims-based current user (ICurrentUser / IAuditUser), and permission-based
authorization where policies resolve to permission names and a user's permissions come from permission groups.
- Package id:
IronAlpine.Framework.Security(3.1.1,net10.0) - Depends on: Kernel, Data,
Microsoft.AspNetCore.Authentication.JwtBearer10
Installation
<PackageReference Include="IronAlpine.Framework.Security" Version="3.1.1" />
Concepts
JWT (HS256) policy name on endpoint permission store
sub, email, name, role, ---> [Authorize(Policy = "OrdersRead")] ---> groups claim (Guid ids)
groups: [g1, g2] resolver: "OrdersRead" -> "orders:read" -> permission names of g1, g2
requirement satisfied if the user owns ANY required permission
- Policy name: the string used in
[Authorize(Policy = ...)]. - Permission name: the value stored in the permission store and compared at runtime.
- Permission group: the user's JWT carries group ids (claim
groupsby default); groups own permissions.
Registration
using IronAlpine.Security.AspNetCore.Extensions;
using IronAlpine.Security.Authorization.EFCore.Extensions;
builder.Services
.AddIronAlpineSecurityJwt(builder.Configuration) // authentication + authorization core
.WithPermissionCatalog(OrderPermissions.All, p => p.PolicyName, p => p.PermissionName)
.WithAuthorizationStoreEfCore<OrdersDbContext>(); // permission projections in the service DB
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
AddIronAlpineSecurityJwt(configuration, configure = null) returns an IronAlpineSecurityBuilder and:
- binds
IronAlpine:Security:AspNetCoreonce at registration time (plusconfigure) and validates thatJwt:Secret,Jwt:IssuerandJwt:Audienceare set (throws otherwise); - adds JWT bearer authentication as the default scheme with
MapInboundClaims = false(claim types stay as in the token:sub,email,role, ...) and a symmetric signing key fromJwt:Secret; - registers
ICurrentUserandIAuditUser(scoped, fromHttpContext.User) andIHttpContextAccessor; - registers authorization:
PermissionPolicyProvider(replacesIAuthorizationPolicyProvider),PermissionAuthorizationHandler,IPermissionCheckerService,DefaultPermissionPolicyNameResolverand a start-up validator of known policies; - copies
Permission:PermissionGroupClaimTypeintoSecurityAuthorizationOptions.
AddIronAlpineSecurityAspNetCore(configure = null) registers only ICurrentUser / IAuditUser (for hosts that
authenticate differently but still want the current-user abstraction and audit stamping).
Builder methods
| Method | Effect |
|---|---|
WithPermissionCatalog(IPermissionCatalog) |
resolver = catalog (CatalogPermissionPolicyNameResolver) |
WithPermissionCatalog<T>(values, policyName, permissionName) |
builds an InMemoryPermissionCatalog from any list |
WithSmartEnumCatalog<T>(values, fieldNameSelector, permissionNameSelector) |
same, named for smart enumerations |
AddPolicyCatalog<TPermission>(prefix = "") |
reflection: policy name = prefix + TPermission.GetFieldName(value), permission name = value.Name, values from static GetValues() |
WithPermissionPolicyResolver<TResolver>() |
custom IPermissionPolicyNameResolver |
WithAuthorizationStoreEfCore<TDbContext>(configure) |
EF Core permission store (see below) |
WithSignalR(params string[] hubPaths) |
accepts access_token from the query string for those paths |
WithAuthorizationCore() |
already called by AddIronAlpineSecurityJwt |
AddPolicyCatalog<TPermission> requires these members on TPermission:
public sealed class OrderPermission : Enumeration<OrderPermission>
{
public static readonly OrderPermission OrdersRead = new(1001, "orders:read");
public static readonly OrderPermission OrdersWrite = new(1002, "orders:write");
private OrderPermission(int id, string name) : base(id, name) { }
public static new IReadOnlyCollection<OrderPermission> GetValues() => Enumeration<OrderPermission>.GetValues();
public new string Name => base.Name;
public static string GetFieldName(OrderPermission permission) =>
typeof(OrderPermission).GetFields(BindingFlags.Public | BindingFlags.Static)
.First(f => f.GetValue(null) == permission).Name;
}
builder.Services.AddIronAlpineSecurityJwt(builder.Configuration).AddPolicyCatalog<OrderPermission>();
[Authorize(Policy = nameof(OrderPermission.OrdersRead))] // policy "OrdersRead" -> permission "orders:read"
The static GetValues() and GetFieldName(T) methods must be declared on the concrete type (static members of the
base class are not found by the reflection lookup).
Configuration
{
"IronAlpine": {
"Security": {
"AspNetCore": {
"Jwt": {
"Issuer": "https://auth.example.com",
"Audience": "orders-api",
"Secret": "<at least 32 bytes - from a secret store>",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": true,
"ValidateIssuerSigningKey": true,
"ClockSkewSeconds": 0
},
"AuditClaims": {
"UserIdKeys": [ "sub", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "oid" ],
"UserNameKey": "name",
"EmailKey": "email",
"RoleKey": "role"
},
"Permission": { "PermissionGroupClaimType": "groups", "StartupValidationEnabled": true },
"SignalR": { "EnableAccessTokenFromQuery": false, "HubPaths": [] }
},
"Authorization": {
"PermissionGroupClaimType": "groups",
"EFCore": {
"PermissionProjectionTableName": "SecurityPermissionProjections",
"PermissionProjectionStateTableName": "SecurityPermissionProjectionSyncStates",
"PermissionProjectionSchema": null,
"PermissionNameMaxLength": 256
}
}
}
}
}
- Only symmetric (HMAC) signing is supported. HS256 needs a key of at least 32 bytes; a shorter
Secretfails token validation. ClockSkewSecondsdefaults to 0 (tokens expire exactly atexp).- With
AddIronAlpineSecurityJwt,Authorization:PermissionGroupClaimTypeis overwritten byAspNetCore:Permission:PermissionGroupClaimType. - Configuration is read when
AddIronAlpineSecurityJwtruns; values must be available (for example from environment variables) before services are registered.
Current user
using IronAlpine.Security.Abstractions.Contracts;
public sealed class GetMyOrdersQueryHandler(ICurrentUser currentUser, IReadRepository<Order> orders)
: IRequestHandler<GetMyOrdersQuery, Result<IReadOnlyList<OrderDto>>>
{
public async Task<Result<IReadOnlyList<OrderDto>>> Handle(GetMyOrdersQuery request, CancellationToken ct)
{
if (currentUser.TryGetUserId() is not { } userId)
{
return Result.Failure<IReadOnlyList<OrderDto>>(TechnicalErrors.Auth.Unauthorized);
}
var items = await orders.GetWhereQuery(o => o.CustomerId == userId, trackChanges: false)
.Select(o => new OrderDto(o.Id, o.Total))
.ToListAsync(ct);
return Result.Success<IReadOnlyList<OrderDto>>(items);
}
}
ICurrentUser : IAuditUser: IsAuthenticated, UserId (first non-empty claim of AuditClaims.UserIdKeys),
UserName, Email, Roles (claim RoleKey), Claims (all claims grouped by type, case-insensitive),
GetClaims(type), GetFirstClaim(type). Extensions: TryGetUserId() (Guid?), GetUserIdOrEmpty(),
GetUserNameOrEmpty(), GetEmailOrEmpty(), GetClaimsAsPrincipalClaims(). All values are empty when there is no
authenticated HTTP user (background services, message handlers).
The Data package uses IAuditUser to stamp CreatedBy / ModifiedBy / DeletedBy.
Authorization
[Authorize(Policy = "OrdersRead")]
public async Task<IActionResult> Get(Guid id) => ...;
[AuthorizeAnyPermission("OrdersWrite", "OrdersAdmin")] // OR: any of the policies
public async Task<IActionResult> Cancel(Guid id) => ...;
PermissionPolicyProvider.GetPolicyAsync(name):
- a policy registered explicitly in
AuthorizationOptions(AddPolicy) is returned as-is, whatever the resolver; - single name: resolver maps it to a permission name → policy
RequireAuthenticatedUser+PermissionRequirement(permission); if the resolver does not know the name, the regularDefaultAuthorizationPolicyProvideris asked (policies added withAddPolicy); - comma-separated names (what
AuthorizeAnyPermissionproduces): one requirement with all permissions (OR); if any name is unknown the whole string is passed to the default provider.
PermissionAuthorizationHandler loads the user's permissions through IPermissionCheckerService (group ids from the
PermissionGroupClaimType claim; non-Guid values are skipped with a warning) and succeeds when the user owns any
required permission (case-insensitive). The lookup runs on every authorization; there is no caching.
Resolvers:
| Resolver | Mapping |
|---|---|
DefaultPermissionPolicyNameResolver (default) |
policy name == permission name, for every non-empty name that is not registered with AddPolicy |
CatalogPermissionPolicyNameResolver |
exact (ordinal) catalog lookup; unknown names fall back to normal policies |
Multiple [Authorize] attributes (for example on the controller and on the action) are combined with AND.
Permission store
ISecurityPermissionStore.GetPermissionNamesByGroupIdsAsync(groupIds) is required by IPermissionCheckerService.
Without a registered store, every permission check fails with a DI resolution error.
WithAuthorizationStoreEfCore<TDbContext>() / services.AddIronAlpineAuthorizationEfCore<TDbContext>() registers:
- tables (model contributor):
SecurityPermissionProjections(PermissionGroupId,PermissionId,PermissionName; key group + permission id) andSecurityPermissionProjectionSyncStates(PermissionGroupId,LastOccurredOnUtc,LastEventId); ISecurityPermissionStore(reads the projection) andISecurityPermissionProjectionSyncStore(writes it).
Keep the projection in sync from the service that owns permission groups (typically through integration events):
public sealed class PermissionGroupChangedHandler(ISecurityPermissionProjectionSyncStore sync, IUnitOfWork unitOfWork)
: INotificationHandler<PermissionGroupChangedIntegrationEvent>
{
public async Task Handle(PermissionGroupChangedIntegrationEvent e, CancellationToken ct)
{
await sync.ReplacePermissionsAsync(
e.GroupId,
e.Permissions.Select(p => new SecurityPermissionProjectionSyncItem(p.Id, p.Name)).ToArray(),
e.OccurredOnUtc,
e.EventId,
ct);
await unitOfWork.SaveChangesAsync(ct); // the sync store does not save
}
}
ReplacePermissionsAsyncreplaces all rows of the group;DeleteGroupAsyncremoves them. Both record the event timestamp/id and ignore stale events (olderoccurredOnUtc, or the same timestamp and event id).- Both silently do nothing for empty ids/timestamps or when the tables are not part of the
DbContextmodel. - Items with
PermissionId <= 0or an empty name are skipped.
A service that owns the permission data itself can implement ISecurityPermissionStore directly and register it
instead of the EF store.
Pitfalls
- The default resolver turns every policy name that is not registered with
AddPolicyinto a permission requirement. A typo in a policy name therefore becomes a permission check instead of a "policy not found" error; use a catalog resolver when you want unknown names to fail. (Up to 3.0.4 the default resolver also shadowed policies registered withAddPolicy.) - Register a permission store; otherwise authorization throws at request time.
StartupValidationEnabledonly checks that catalog entries resolve; it does not detect policy names used on endpoints but missing from the catalog. Such endpoints fall back to the default provider and fail with "policy not found" at request time.WithSignalRreplacesJwtBearerEvents.OnMessageReceived; combine your own handler in the same delegate if needed.- Tokens are validated with a shared secret: every service that validates tokens needs the same
Jwt:Secret,IssuerandAudience.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- IronAlpine.Framework.Data (>= 3.1.1)
- IronAlpine.Framework.Kernel (>= 3.1.1)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.10)
- Microsoft.EntityFrameworkCore (>= 10.0.10)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.1.1 | 191 | 10/21/2026 |
| 3.0.9 | 215 | 10/15/2026 |
| 3.0.8 | 117 | 10/13/2026 |
| 3.0.7 | 104 | 10/13/2026 |
| 3.0.6 | 109 | 10/9/2026 |
| 3.0.5 | 183 | 10/5/2026 |
| 3.0.4 | 198 | 10/2/2026 |
| 3.0.3 | 165 | 9/30/2026 |
| 3.0.2 | 1,000 | 7/22/2026 |
| 3.0.1 | 127 | 7/22/2026 |
| 3.0.0 | 336 | 7/20/2026 |
| 2.0.3 | 125 | 7/20/2026 |
| 2.0.2 | 154 | 7/13/2026 |
| 2.0.1 | 156 | 7/13/2026 |
| 2.0.0 | 145 | 7/13/2026 |
| 2.0.0-preview.2 | 74 | 7/13/2026 |
| 2.0.0-preview.1 | 75 | 7/6/2026 |
| 1.0.2 | 125 | 7/3/2026 |
| 1.0.1 | 124 | 7/3/2026 |
IronAlpine framework release. See the package README and repository documentation for details.