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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IronAlpine.Framework.Security" Version="3.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="IronAlpine.Framework.Security" Version="3.1.1" />
                    
Directory.Packages.props
<PackageReference Include="IronAlpine.Framework.Security" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add IronAlpine.Framework.Security --version 3.1.1
                    
#r "nuget: IronAlpine.Framework.Security, 3.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package IronAlpine.Framework.Security@3.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=IronAlpine.Framework.Security&version=3.1.1
                    
Install as a Cake Addin
#tool nuget:?package=IronAlpine.Framework.Security&version=3.1.1
                    
Install as a Cake Tool

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.JwtBearer 10

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 groups by 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:AspNetCore once at registration time (plus configure) and validates that Jwt:Secret, Jwt:Issuer and Jwt:Audience are 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 from Jwt:Secret;
  • registers ICurrentUser and IAuditUser (scoped, from HttpContext.User) and IHttpContextAccessor;
  • registers authorization: PermissionPolicyProvider (replaces IAuthorizationPolicyProvider), PermissionAuthorizationHandler, IPermissionCheckerService, DefaultPermissionPolicyNameResolver and a start-up validator of known policies;
  • copies Permission:PermissionGroupClaimType into SecurityAuthorizationOptions.

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 Secret fails token validation.
  • ClockSkewSeconds defaults to 0 (tokens expire exactly at exp).
  • With AddIronAlpineSecurityJwt, Authorization:PermissionGroupClaimType is overwritten by AspNetCore:Permission:PermissionGroupClaimType.
  • Configuration is read when AddIronAlpineSecurityJwt runs; 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 regular DefaultAuthorizationPolicyProvider is asked (policies added with AddPolicy);
  • comma-separated names (what AuthorizeAnyPermission produces): 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) and SecurityPermissionProjectionSyncStates (PermissionGroupId, LastOccurredOnUtc, LastEventId);
  • ISecurityPermissionStore (reads the projection) and ISecurityPermissionProjectionSyncStore (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
    }
}
  • ReplacePermissionsAsync replaces all rows of the group; DeleteGroupAsync removes them. Both record the event timestamp/id and ignore stale events (older occurredOnUtc, or the same timestamp and event id).
  • Both silently do nothing for empty ids/timestamps or when the tables are not part of the DbContext model.
  • Items with PermissionId <= 0 or 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 AddPolicy into 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 with AddPolicy.)
  • Register a permission store; otherwise authorization throws at request time.
  • StartupValidationEnabled only 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.
  • WithSignalR replaces JwtBearerEvents.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, Issuer and Audience.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

IronAlpine framework release. See the package README and repository documentation for details.