hexasync.auth.middleware 2604.27.3
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>();
// Note: UseCapabilities() middleware is NO LONGER NEEDED!
// Capabilities are resolved automatically by HexaSyncAuthenticationHandler
app.UseAuthentication();
app.UseAuthorization();
// Capability resolution happens during authentication - no middleware call needed
Note
UseCapabilities() middleware is deprecated. Capability resolution is now performed automatically by HexaSyncAuthenticationHandler during authentication, providing better performance.
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 authentication handler 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 10.0
- Microsoft.AspNetCore.Mvc.Abstractions (>= 2.3.9)
- Microsoft.Extensions.Http (>= 10.0.5)
- hexasync.services.proxy (>= 1.10.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 |