hexasync.auth.middleware 2603.15.2

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

  1. HTTPS Required: Always use HTTPS for the JWKS endpoint in production
  2. Secret Management: Store HMAC secrets securely (e.g., Azure Key Vault, AWS Secrets Manager)
  3. Key Rotation: JWKS keys are cached; the cache refreshes automatically
  4. 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 JwksCacheDuration based 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 options
  • IDistributedCache - In-memory cache (can be overridden with Redis, etc.)
  • IHttpClientFactory - HTTP client for calling SSO API
  • ICapabilityResolver - 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_role claim)
  • 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.

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