IronAlpine.Security
3.0.0
dotnet add package IronAlpine.Security --version 3.0.0
NuGet\Install-Package IronAlpine.Security -Version 3.0.0
<PackageReference Include="IronAlpine.Security" Version="3.0.0" />
<PackageVersion Include="IronAlpine.Security" Version="3.0.0" />
<PackageReference Include="IronAlpine.Security" />
paket add IronAlpine.Security --version 3.0.0
#r "nuget: IronAlpine.Security, 3.0.0"
#:package IronAlpine.Security@3.0.0
#addin nuget:?package=IronAlpine.Security&version=3.0.0
#tool nuget:?package=IronAlpine.Security&version=3.0.0
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 | Versions 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. |
-
net10.0
- IronAlpine.Data (>= 3.0.0)
- IronAlpine.Kernel (>= 3.0.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 9.0.7)
- Microsoft.EntityFrameworkCore (>= 9.0.7)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.7)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.7)
-
net9.0
- IronAlpine.Data (>= 3.0.0)
- IronAlpine.Kernel (>= 3.0.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 9.0.7)
- Microsoft.EntityFrameworkCore (>= 9.0.7)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.7)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.7)
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.