IronAlpine.Security 3.0.0

dotnet add package IronAlpine.Security --version 3.0.0
                    
NuGet\Install-Package IronAlpine.Security -Version 3.0.0
                    
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.Security" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="IronAlpine.Security" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="IronAlpine.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.Security --version 3.0.0
                    
#r "nuget: IronAlpine.Security, 3.0.0"
                    
#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.Security@3.0.0
                    
#: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.Security&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=IronAlpine.Security&version=3.0.0
                    
Install as a Cake Tool

IronAlpine.Security

JWT authentication, claim-based authorization, and permission management.

  • Target Frameworks: net9.0, net10.0
  • Dependencies: ASP.NET Core, EntityFrameworkCore
  • Package Size: ~90 KB
  • Replaces: Security (all layers consolidated)

What It Is

IronAlpine.Security provides identity and authorization:

  • JWT Authentication — Bearer token validation with configurable issuer/audience
  • Current User Context — ICurrentUser for accessing authenticated principal
  • Policy-Based Authorization — [Authorize(Policy = "...")] attributes
  • Permission Catalog — central permission registry
  • Audit User — who and when tracking for audit logs

Installation

dotnet add package IronAlpine.Security

Quick Setup

services.AddIronAlpineSecurityJwt(configuration);
services.AddPolicyCatalog<TimeOffPermissions>();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

Configuration

JWT Settings

{
  "IronAlpine": {
    "Security": {
      "AspNetCore": {
        "Jwt": {
          "Issuer": "https://dps.identity.local",
          "Audience": "https://api.example.com",
          "AccessTokenExpirationMinutes": 15,
          "RefreshTokenExpirationDays": 7,
          "ValidateLifetime": true,
          "ValidateIssuer": true,
          "ValidateAudience": true,
          "ClockSkewSeconds": 30
        },
        "Permission": {
          "PermissionGroupClaimType": "groups",
          "PermissionNameClaimType": "permissions",
          "StartupValidationEnabled": true
        }
      }
    }
  }
}
Setting Default Purpose
Issuer - JWT token issuer
Audience - JWT token audience
AccessTokenExpirationMinutes 15 Token lifetime
ValidateIssuer true Verify issuer matches config
ValidateAudience true Verify audience matches
ValidateLifetime true Check expiration

Define Permissions

public static class TimeOffPermissions
{
    // Request operations
    public const string RequestTimeOff = "timeoff:request";
    public const string ViewOwnTimeOff = "timeoff:view:own";
    public const string ViewAllTimeOff = "timeoff:view:all";

    // Approval operations
    public const string ApproveTimeOff = "timeoff:approve";
    public const string RejectTimeOff = "timeoff:reject";

    // Admin operations
    public const string DeleteTimeOff = "timeoff:delete";
    public const string ExportTimeOff = "timeoff:export";
}

Authenticate Requests

Get Current User

[ApiController]
[Route("api/[controller]")]
public class TimeOffController : ControllerBase
{
    private readonly ICurrentUser _currentUser;

    public TimeOffController(ICurrentUser currentUser)
    {
        _currentUser = currentUser;
    }

    /// <summary>
    /// Get current authenticated user info
    /// </summary>
    [HttpGet("me")]
    [Authorize]
    public IActionResult GetCurrentUser()
    {
        return Ok(new
        {
            UserId = _currentUser.UserId,
            Name = _currentUser.Name,
            Email = _currentUser.Email,
            IsAuthenticated = _currentUser.IsAuthenticated
        });
    }
}

ICurrentUser Interface

public interface ICurrentUser : IAuditUser
{
    // IAuditUser (from Kernel)
    string? UserId { get; }              // Current user ID
    bool IsAuthenticated { get; }        // Is user logged in

    // ICurrentUser (extended)
    string? Name { get; }                // User full name
    string? Email { get; }               // User email
    Dictionary<string, string> Claims { get; }  // All claims
    List<string> Roles { get; }          // User roles
}

Authorize Endpoints

Basic Authorization

[HttpPost("request")]
[Authorize]  // Any authenticated user
public async Task<IActionResult> Request(RequestTimeOffCommand command)
{
    command.UserId = Guid.Parse(_currentUser.UserId!);
    await _mediator.Send(command);
    return Ok();
}

Policy-Based Authorization

[HttpPost("request")]
[Authorize(Policy = TimeOffPermissions.RequestTimeOff)]
public async Task<IActionResult> Request(RequestTimeOffCommand command)
{
    // Only users with "timeoff:request" permission can access
}

[HttpGet]
[Authorize(Policy = TimeOffPermissions.ViewAllTimeOff)]
public async Task<IActionResult> ListAll()
{
    // Only managers can view all (others get 403 Forbidden)
}

[HttpPost("{id}/approve")]
[Authorize(Policy = TimeOffPermissions.ApproveTimeOff)]
public async Task<IActionResult> Approve(Guid id, ApproveTimeOffCommand command)
{
    // Only approvers can approve
}

Multiple Policies (AND)

[HttpPost("{id}/delete")]
[Authorize(Policy = TimeOffPermissions.DeleteTimeOff)]
[Authorize(Policy = "IsAdmin")]
public async Task<IActionResult> Delete(Guid id)
{
    // Requires BOTH DeleteTimeOff AND IsAdmin permissions
}

Role-Based Authorization

[HttpPost("admin-only")]
[Authorize(Roles = "Admin,Manager")]
public async Task<IActionResult> AdminOperation()
{
    // Users with Admin or Manager role
}

Use in Domain/Application Layers

Audit User Context

public class TimeOffRepository : Repository<TimeOff>, ITimeOffRepository
{
    private readonly ICurrentUser _currentUser;

    public TimeOffRepository(TimeOffContext context, ICurrentUser currentUser)
        : base(context)
    {
        _currentUser = currentUser;
    }

    public async Task<IEnumerable<TimeOff>> ListByUserAsync(CancellationToken ct)
    {
        // Only return time offs requested by current user
        var spec = new TimeOffByUserSpec(_currentUser.UserId);
        return await ListAsync(spec, ct);
    }
}

Track Changes

public class TimeOffContext : DbContext
{
    private readonly ICurrentUser _currentUser;

    public TimeOffContext(
        DbContextOptions<TimeOffContext> options,
        ICurrentUser currentUser)
        : base(options)
    {
        _currentUser = currentUser;
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // Automatically fill audit fields
        foreach (var entry in ChangeTracker.Entries())
        {
            if (entry.Entity is IAuditable auditable)
            {
                if (entry.State == EntityState.Added)
                {
                    auditable.CreatedBy = _currentUser.UserId ?? "system";
                    auditable.CreatedAt = DateTime.UtcNow;
                }
                else if (entry.State == EntityState.Modified)
                {
                    auditable.ModifiedBy = _currentUser.UserId ?? "system";
                    auditable.ModifiedAt = DateTime.UtcNow;
                }
            }
        }

        return await base.SaveChangesAsync(cancellationToken);
    }
}

JWT Token Structure

Standard Claims

JWT tokens include standard OpenID Connect claims:

{
  "sub": "00000000-0000-0000-0000-000000000001",    // Subject (user ID)
  "name": "John Doe",                               // Full name
  "email": "john@example.com",                      // Email
  "aud": "https://api.example.com",                 // Audience
  "iss": "https://dps.identity.local",              // Issuer
  "iat": 1234567890,                                // Issued at
  "exp": 1234568890                                 // Expires
}

Custom Claims

Application-specific claims:

{
  "sub": "00000000-0000-0000-0000-000000000001",
  "name": "John Doe",
  "email": "john@example.com",
  "groups": ["TimeOff.Managers", "Payroll.Readers"],
  "permissions": ["timeoff:approve", "timeoff:reject", "payroll:view"],
  "tenant_id": "tenant-001"
}

Access Token

// Issued by Identity Service
var token = new JwtSecurityToken(
    issuer: "dps.identity",
    audience: "api.example.com",
    claims: new[]
    {
        new Claim(ClaimTypes.NameIdentifier, userId),
        new Claim(ClaimTypes.Name, userName),
        new Claim(ClaimTypes.Email, userEmail),
        new Claim("groups", "Manager"),
        new Claim("groups", "Approver")
    },
    expires: DateTime.UtcNow.AddMinutes(15),
    signingCredentials: creds);

var jwt = handler.WriteToken(token);

Permission Catalog

Central registry of all permissions for validation:

public class TimeOffPermissions
{
    public const string RequestTimeOff = "timeoff:request";
    public const string ApproveTimeOff = "timeoff:approve";
    public const string ViewAllTimeOff = "timeoff:view:all";
    
    public static IEnumerable<string> All => new[]
    {
        RequestTimeOff,
        ApproveTimeOff,
        ViewAllTimeOff
    };
}

// Register
services.AddPolicyCatalog<TimeOffPermissions>();

// Framework validates at startup:
// ✅ All permissions are present
// ✅ No duplicate permissions
// ✅ Naming conventions followed

Error Responses

401 Unauthorized (Not Authenticated)

{
  "type": "https://api.example.com/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Missing or invalid bearer token",
  "traceId": "0HN48G5HMIB62:00000001"
}

403 Forbidden (No Permission)

{
  "type": "https://api.example.com/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "User does not have required permission: timeoff:approve",
  "traceId": "0HN48G5HMIB62:00000001"
}

Security Best Practices

✅ DO

// 1. Check permissions early
[HttpPost("approve")]
[Authorize(Policy = TimeOffPermissions.ApproveTimeOff)]
public async Task<IActionResult> Approve(Guid id) { }

// 2. Use dependency injection for ICurrentUser
public class MyHandler
{
    private readonly ICurrentUser _currentUser;
    
    public MyHandler(ICurrentUser currentUser)
    {
        _currentUser = currentUser;
    }
}

// 3. Validate user context in repositories
if (!_currentUser.IsAuthenticated)
    throw new UnauthorizedException("User not authenticated");

// 4. Use JWT bearer tokens
// Don't use cookies for API authentication

// 5. Validate token claims
var userId = _currentUser.UserId;
Guard.Against.Null(userId, nameof(userId));

❌ DON'T

// 1. Don't skip authorization
[HttpPost("delete")]
public async Task<IActionResult> Delete(Guid id) { }  // Unprotected!

// 2. Don't embed user ID in request
var userId = request.UserId;  // User could change this
// Use: _currentUser.UserId instead

// 3. Don't trust client claims
var role = Request.Form["role"];  // Client could fake this
// Use: _currentUser.Roles from token

// 4. Don't expose internal errors
catch (Exception ex)
{
    return BadRequest(ex.Message);  // Reveals internals
}
// Return: ProblemDetails from middleware

// 5. Don't store sensitive data in JWT
var token = "{ password: '...' }";  // JWT is base64 encoded (visible!)
// Only store: sub, name, email, roles, groups

Troubleshooting

Q: Token validation failing?
A: Check (1) Issuer matches config, (2) Audience correct, (3) Token not expired, (4) Signature valid.

Q: Permissions not working?
A: Verify (1) policies registered, (2) claims in token, (3) [Authorize(Policy = ...)] spelled correctly.

Q: ICurrentUser always null?
A: Ensure (1) AddIronAlpineSecurityJwt() called, (2) UseAuthentication() before UseAuthorization().

Q: CORS + Authentication not working together?
A: Add CORS headers to preflight responses. Check OPTIONS /api/... returns 200.

Examples

See IRONALPINE_V3_DOCUMENTATION.md for detailed examples.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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.0.0 126 7/1/2026

Stable mediator release with request/response, notification publish strategies, streaming, and dependency injection integration.