hexasync.auth.middleware 2512.12.1
HexaSync Auth Middleware
Authentication middleware for HexaSync services. Supports dual-mode token validation (ECDSA/JWKS and HMAC) for graceful migration from legacy HMAC tokens to ECDSA/JWKS tokens.
Installation
dotnet add package hexasync.auth.middleware
Or add to your .csproj:
<PackageReference Include="hexasync.auth.middleware" Version="1.0.0" />
Quick Start
1. Zero-Config (Environment Variables)
The simplest setup relies entirely on environment variables. This is perfect for containers and cloud deployments.
# Required
SSO_URL=https://sso.hexasync.com
SSO_SERVICE_SECRET=your-hmac-secret # For legacy HMAC support
# Optional
JWT_ISSUER=https://sso.hexasync.com # Defaults to same as SSO_URL if omitted
JWT_AUDIENCE=your-service-audience # Validates aud claim if provided
// Program.cs
builder.Services.AddHexaSyncAuthentication();
2. Manual Configuration (Delegate)
For more control or when not using environment variables:
builder.Services.AddHexaSyncAuthentication(options =>
{
options.SsoUrl = "https://sso.hexasync.com";
options.HmacSecretKey = builder.Configuration["Auth:HmacSecret"];
options.ValidationMode = TokenValidationMode.DualMode;
});
ECDSA Only (Post-Migration)
After all tokens have been migrated to ECDSA:
builder.Services.AddHexaSyncAuthentication(options =>
{
options.JwksEndpoint = "https://sso.hexasync.com/.well-known/jwks.json";
options.Issuer = "https://sso.hexasync.com";
options.ValidationMode = TokenValidationMode.EcdsaOnly;
});
HMAC Only (Legacy)
For services that only need HMAC validation:
builder.Services.AddHexaSyncAuthentication(options =>
{
options.HmacSecretKey = builder.Configuration["Auth:HmacSecret"];
options.Issuer = "https://sso.hexasync.com";
options.ValidationMode = TokenValidationMode.HmacOnly;
});
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
JwksEndpoint |
string | null | URL of the JWKS endpoint |
HmacSecretKey |
string | null | HMAC secret for legacy tokens |
Issuer |
string | null | Expected token issuer |
Audiences |
string[] | null | Expected token audiences |
ValidationMode |
enum | DualMode | ECDSA, HMAC, or DualMode |
ValidateIssuer |
bool | true | Validate issuer claim |
ValidateAudience |
bool | true | Validate audience claim |
ValidateLifetime |
bool | true | Validate token expiration |
ClockSkew |
TimeSpan | 5 min | Clock skew tolerance |
JwksCacheDuration |
TimeSpan | 24 hours | JWKS cache duration (safe due to staging period) |
JwksRequestTimeout |
TimeSpan | 30 sec | JWKS fetch timeout |
RequireHttpsMetadata |
bool | true | Require HTTPS for JWKS |
Multiple Authentication Schemes
If you need to support multiple authentication schemes:
builder.Services.AddAuthentication()
.AddHexaSync("HexaSync", options =>
{
options.JwksEndpoint = "https://sso.hexasync.com/.well-known/jwks.json";
options.HmacSecretKey = builder.Configuration["Auth:HmacSecret"];
options.ValidationMode = TokenValidationMode.DualMode;
})
.AddJwtBearer("AnotherScheme", options =>
{
// Other JWT configuration
});
Migration Guide
Phase 1: Deploy Middleware (Dual Mode)
Deploy this middleware to all services with DualMode enabled:
options.ValidationMode = TokenValidationMode.DualMode;
Services can now validate both HMAC and ECDSA tokens.
Phase 2: Deploy V3 SSO Endpoints
Deploy the new SSO V3 endpoints that issue ECDSA tokens. New tokens will be validated via JWKS, legacy tokens via HMAC.
Phase 3: Switch to ECDSA Only
After all legacy tokens have expired (30 days), switch to ECDSA only:
options.ValidationMode = TokenValidationMode.EcdsaOnly;
options.HmacSecretKey = null; // Remove HMAC secret
Debugging
The middleware adds a token_validation_method claim to indicate which method was used:
var method = User.FindFirst("token_validation_method")?.Value;
// Returns "ECDSA" or "HMAC"
Security Considerations
- HTTPS Required: Always use HTTPS for the JWKS endpoint in production
- Secret Management: Store HMAC secrets securely (e.g., Azure Key Vault, AWS Secrets Manager)
- Key Rotation: JWKS keys are cached; the cache refreshes automatically
- Clock Skew: Default 5-minute tolerance; adjust based on your infrastructure
Troubleshooting
JWKS Fetch Fails
Check:
- Network connectivity to JWKS endpoint
- HTTPS certificate validity
- Firewall rules
Token Validation Fails
Check:
- Token expiration
- Issuer/audience mismatch
- Clock synchronization between services
Performance
- JWKS keys are cached for 1 hour by default
- Adjust
JwksCacheDurationbased on your key rotation frequency
Client Credentials (Service-to-Service Auth)
Obtain access tokens for service-to-service communication using OAuth client credentials flow.
Configuration
# Environment variables
SSO_URL=https://sso.hexasync.com
SSO_CLIENT_ID=your-service-client-id
SSO_CLIENT_SECRET=your-service-client-secret
Setup
// Program.cs
// Option 1: Use IConfiguration (recommended)
builder.Services.AddClientCredentials(builder.Configuration);
// Option 2: Manual configuration
builder.Services.AddClientCredentials(options =>
{
options.ClientId = "your-client-id";
options.ClientSecret = "your-client-secret";
options.SsoUrl = "https://sso.hexasync.com";
});
// Option 3: Explicit values
builder.Services.AddClientCredentials(
clientId: "your-client-id",
clientSecret: "your-client-secret");
Usage
public class MyService
{
private readonly IClientCredentialsService _tokenService;
private readonly HttpClient _httpClient;
public MyService(IClientCredentialsService tokenService, HttpClient httpClient)
{
_tokenService = tokenService;
_httpClient = httpClient;
}
public async Task CallProtectedApiAsync()
{
// Get access token (automatically cached until near expiration)
var token = await _tokenService.GetAccessTokenAsync();
// Use token in API call
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.GetAsync("https://api.example.com/data");
}
}
Capability-Based Authorization
The middleware includes a capability-based authorization system that works with the system_role JWT claim.
Configuration
Set environment variables or appsettings.json:
# Environment variables (optional - defaults shown)
SSO_URL=https://sso.hexasync.com
SSO_CAPABILITY_CACHE_MINUTES=5
SSO_REQUEST_TIMEOUT_SECONDS=10
Or in appsettings.json:
{
"Capabilities": {
"SsoUrl": "https://sso.hexasync.com",
"CacheMinutes": 5,
"RequestTimeoutSeconds": 10,
"EnableCaching": true
}
}
Setup
// Program.cs
// Option 1: Use IConfiguration (recommended - reads from env vars and appsettings)
builder.Services.AddCapabilityResolution(builder.Configuration);
// Option 2: Manual configuration
builder.Services.AddCapabilityResolution(options =>
{
options.SsoUrl = "https://sso.hexasync.com";
options.CacheMinutes = 5;
});
// Option 3: Simple setup with just URL
builder.Services.AddCachedCapabilityResolution(
ssoBaseUrl: "https://sso.hexasync.com",
cacheDuration: TimeSpan.FromMinutes(5));
// Option 4: Custom resolver (e.g., direct database access)
builder.Services.AddCapabilityResolution<MyCapabilityResolver>();
// Add middleware (after UseAuthorization)
app.UseAuthentication();
app.UseAuthorization();
app.UseCapabilities(); // Resolves capabilities from system_role claim
What Gets Registered
When you call AddCapabilityResolution(IConfiguration), the following services are automatically registered:
IOptions<CapabilityOptions>- Configuration optionsIDistributedCache- In-memory cache (can be overridden with Redis, etc.)IHttpClientFactory- HTTP client for calling SSO APIICapabilityResolver- The capability resolver (cached or non-cached based on config)
Protecting Endpoints
// Minimal API - OR logic (requires ANY of the capabilities)
app.MapGet("/users", GetUsers)
.RequireCapability("users:read", "admin:full_access");
// Minimal API - AND logic (requires ALL capabilities)
app.MapPost("/users", CreateUser)
.RequireAllCapabilities("users:write", "audit:enabled");
// MVC Controller
[RequireCapability("users:read", "admin:full_access")]
public IActionResult GetUsers() { ... }
// MVC Controller - AND logic
[RequireCapability("users:write", "audit:enabled", RequireAll = true)]
public IActionResult CreateUser() { ... }
Checking Capabilities in Code
Use the AppCapabilities class for type-safe capability references.
using hexasync.auth.middleware.Capabilities;
// Check single capability
if (httpContext.HasCapability(AppCapabilities.Sso.OAuthApps.Write))
{
// User can write oauth apps
}
// Check any capability (OR)
if (httpContext.HasAnyCapability(
AppCapabilities.Sso.OAuthApps.Read,
"admin:full_access"))
{
// User has access
}
// Check all capabilities (AND)
if (httpContext.HasAllCapabilities(
AppCapabilities.Sso.OAuthApps.Read,
AppCapabilities.Sso.OAuthApps.Write))
{
// User has both read and write access
}
// Get all capabilities
var capabilities = httpContext.GetCapabilities();
// Get role ID from claims
var roleId = httpContext.GetSystemRoleId();
Capability Resolution Flow
The middleware resolves capabilities based on token type:
| Token Type | JWT Claims | Resolution |
|---|---|---|
| User OAuth | sub, client_id, system_role |
User caps ∩ App caps |
| Client Credentials | client_id, system_role |
App caps (from role) |
| SSO UI (first-party) | sub, client_id, system_role |
User caps only |
For User OAuth tokens, the effective capabilities are the intersection of:
- User's capabilities (from
system_roleclaim) - App's capabilities (from
client_id→ app's role)
This ensures third-party apps can never exceed the user's permissions.
Custom Capability Resolver
Implement ICapabilityResolver for custom resolution logic:
public class MyCapabilityResolver : ICapabilityResolver
{
private readonly IDistributedCache _cache;
private readonly MyDbContext _db;
public async Task<IReadOnlyList<string>> GetCapabilitiesAsync(
Guid roleId,
CancellationToken cancellationToken = default)
{
// Check cache first
var cacheKey = $"caps:{roleId}";
var cached = await _cache.GetStringAsync(cacheKey, cancellationToken);
if (!string.IsNullOrEmpty(cached))
{
return cached.Split(',');
}
// Query database
var capabilities = await _db.RoleCapabilities
.Where(rc => rc.RoleId == roleId)
.Join(_db.Capabilities, rc => rc.CapabilityId, c => c.Id, (rc, c) => c.Name)
.ToListAsync(cancellationToken);
// Cache result
await _cache.SetStringAsync(cacheKey, string.Join(",", capabilities),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) },
cancellationToken);
return capabilities;
}
public async Task<IReadOnlyList<string>> GetAppCapabilitiesAsync(
string clientId,
CancellationToken cancellationToken = default)
{
// Resolve app's role and get capabilities
var app = await _db.OAuthApplications
.Where(a => a.ClientId == clientId)
.Select(a => a.RoleId)
.FirstOrDefaultAsync(cancellationToken);
if (app == null) return Array.Empty<string>();
return await GetCapabilitiesAsync(app.Value, cancellationToken);
}
}
License
MIT
No packages depend on hexasync.auth.middleware.
.NET 8.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.1)
- Microsoft.AspNetCore.Mvc.Abstractions (>= 2.2.0)
- Microsoft.Extensions.Caching.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.IdentityModel.Tokens (>= 8.0.1)
- System.IdentityModel.Tokens.Jwt (>= 8.0.1)
| Version | Downloads | Last updated |
|---|---|---|
| 2604.27.4 | 9 | 04/27/2026 |
| 2604.27.3 | 1 | 04/27/2026 |
| 2604.27.2 | 1 | 04/27/2026 |
| 2604.12.1 | 125 | 04/12/2026 |
| 2604.11.1 | 2 | 04/11/2026 |
| 2604.10.11 | 0 | 04/11/2026 |
| 2604.10.1-pre.1 | 5 | 04/10/2026 |
| 2604.8.2 | 17 | 04/08/2026 |
| 2604.3.2 | 28 | 04/03/2026 |
| 2603.23.3 | 3 | 03/23/2026 |
| 2603.23.2 | 0 | 03/23/2026 |
| 2603.23.1 | 0 | 03/23/2026 |
| 2603.19.2 | 34 | 03/19/2026 |
| 2603.19.1 | 0 | 03/19/2026 |
| 2603.18.1 | 46 | 03/18/2026 |
| 2603.16.3 | 131 | 03/16/2026 |
| 2603.15.2 | 2 | 03/15/2026 |
| 2603.13.1 | 1 | 03/13/2026 |
| 2602.26.3 | 10 | 03/12/2026 |
| 2602.24.1 | 0 | 03/14/2026 |
| 2602.13.1 | 0 | 03/14/2026 |
| 2602.11.1 | 0 | 03/14/2026 |
| 2602.9.2 | 0 | 03/14/2026 |
| 2601.15.1 | 48 | 03/09/2026 |
| 2601.7.2 | 0 | 03/14/2026 |
| 2601.7.1 | 0 | 03/14/2026 |
| 2601.2.1 | 0 | 03/14/2026 |
| 2601.1.2 | 0 | 03/14/2026 |
| 2601.1.1 | 0 | 03/14/2026 |
| 2512.28.1 | 0 | 03/14/2026 |
| 2512.27.3 | 0 | 03/14/2026 |
| 2512.27.2 | 0 | 03/14/2026 |
| 2512.27.1 | 0 | 03/14/2026 |
| 2512.26.1 | 0 | 03/14/2026 |
| 2512.24.1 | 0 | 03/14/2026 |
| 2512.20.1 | 51 | 03/09/2026 |
| 2512.13.1 | 0 | 03/14/2026 |
| 2512.12.2 | 0 | 03/14/2026 |
| 2512.12.1 | 0 | 03/14/2026 |
| 2512.11.4-pre | 0 | 03/14/2026 |
| 2512.11.2-pre | 0 | 03/14/2026 |