hexasync.auth.middleware 2610.7.1

HexaSync Auth Middleware

Authentication middleware for HexaSync services. Validates JWTs signed with ECDSA (ES256/384/512) against the SSO JWKS endpoint. HMAC (HS256/384/512) validation was removed — an HS* token is refused as an unsupported algorithm, under every configuration.

Scope of this document: everything below describes the package as it is implemented today. If a knob is not listed here, it does not exist.

Installation

The package is published to the private feed https://nuget.hexasync.com/, not nuget.org — add it as a source first.

dotnet add package hexasync.auth.middleware --version 2609.7.1

Or in your .csproj — versions are calendar-based (YYMM.D.N, day not zero-padded: 2609.7.1 is 2026-09-07), so pin the current one rather than a semver-looking value:

<PackageReference Include="hexasync.auth.middleware" Version="2609.7.1" />

Quick Start

SSO_SERVICE_URL=https://sso.hexasync.com    # SSO_URL is accepted as a legacy fallback
JWT_ISSUER=...                              # optional; see default in the table below
JWT_AUDIENCE=...                            # optional; see default in the table below
// Program.cs
builder.Services.AddHexaSyncAuthentication();

These are the only options this package reads from the environment — three options, across four variable names (SSO_URL is a legacy alias for SSO_SERVICE_URL). SSO_SERVICE_SECRET is not among them: it keyed HMAC validation, which no longer exists. The package never binds IConfiguration itself, so the [ConfigurationKeyName] attributes on the options class are inert unless you bind configuration yourself — and bind the root, not the HexaSyncAuth section, because those attributes name flat keys (SSO_SERVICE_URL, not HexaSyncAuth__SsoUrl).

Configuring in code

builder.Services.AddHexaSyncAuthentication(options =>
{
    options.SsoUrl = "https://sso.hexasync.com";
});

Important

The overloads that take an Action<HexaSyncAuthOptions> do not read environment variables. If you switch from AddHexaSyncAuthentication() to AddHexaSyncAuthentication(options => …), SSO_SERVICE_URL, JWT_ISSUER and JWT_AUDIENCE stop being applied and every option falls back to its default — including SsoUrl, which drives the JWKS endpoint. Set every value you need in the delegate, or use an environment-binding overload.

Reads environment variables Does not
AddHexaSyncAuthentication() AddHexaSyncAuthentication(Action<…>)
AddHexaSyncAuthentication(scheme) AddHexaSyncAuthentication(scheme, Action<…>)
AddHexaSync(builder) AddHexaSync(builder, Action<…>)
AddHexaSync(builder, scheme) AddHexaSync(builder, scheme, Action<…>)

Configuration Options

Option Type Default Notes
SsoUrl string https://sso.hexasync.com Env: SSO_SERVICE_URL, or SSO_URL (legacy)
JwksEndpoint string computed Read-only. Always {SsoUrl}/.well-known/jwks.json. Change SsoUrl to change it
Issuer string HEXASYNC-IPAAS-WORLD-TOP-DATA-INTEGRATION Env: JWT_ISSUER
Audience string HEXASYNC-IPAAS-WORLD-TOP-DATA-INTEGRATION Env: JWT_AUDIENCE
Audiences IEnumerable<string>? null Takes precedence over Audience when set
ValidateIssuer bool true
ValidateAudience bool true
ValidateLifetime bool true
ClockSkew TimeSpan 5 minutes
JwksCacheDuration TimeSpan 24 hours Absolute expiry. A non-configurable 6-hour sliding expiry applies as well: the entry is dropped after 6 h with no lookups, so a service idle longer than that refetches on its next request. Eviction is lazy — there is no background refresh
JwksRequestTimeout TimeSpan 30 seconds
RequireHttpsMetadata bool true ⚠️ Declared but never read. Nothing enforces it. Ensure SsoUrl is HTTPS yourself
AuthenticationScheme string HexaSync ⚠️ Declared but never read. Setting this property has no effect; the scheme is whatever you pass to AddHexaSync*(scheme), defaulting to HexaSync

Multiple Authentication Schemes

builder.Services.AddAuthentication()
    .AddHexaSync("HexaSync", options =>
    {
        options.SsoUrl = "https://sso.hexasync.com";
    })
    .AddJwtBearer("AnotherScheme", options =>
    {
        // Other JWT configuration — AddJwtBearer needs
        // Microsoft.AspNetCore.Authentication.JwtBearer, which this package does not reference
    });

Note

The two families differ in more than env binding. AddHexaSyncAuthentication* also sets DefaultAuthenticateScheme and DefaultChallengeScheme to the scheme it registers. The AddHexaSync(builder, …) family does not — it only adds the scheme, leaving the defaults to whatever AddAuthentication(...) established. Use the builder family when HexaSync should not be the default.

What AddHexaSync* Registers

  • IJwksKeyProvider → JwksKeyProvider (via AddHttpClient, so the concrete registration is transient)
  • ITokenValidator → EcdsaTokenValidator (singleton)
  • IEcdsaInnerServiceTokenResolver → StandardEcdsaInnerServiceTokenResolver (scoped, TryAdd)
  • IMemoryCache
  • The HexaSync authentication scheme and its handler

Token Validation

Validation routes on the JWT header alg:

alg Path
ES256, ES384, ES512 JWKS keys from {SsoUrl}/.well-known/jwks.json. If kid is present and cached, only that key is used; if the kid is not in the cache, all keys are tried. On SecurityTokenSignatureKeyNotFoundException the keys are refreshed and validation is retried once
HS256, HS384, HS512 Refused. HMAC validation was removed — see Security Considerations
anything else Refused

The handler adds a token_validation_method claim recording which path succeeded:

var method = User.FindFirst("token_validation_method")?.Value;   // always "ECDSA"

Capability-Based Authorization

Capabilities are resolved automatically during authentication by HexaSyncAuthenticationHandler — there is no middleware to add and no separate registration call in this package.

Prerequisite

Resolution needs a working IEcdsaInnerServiceTokenResolver. AddHexaSync* registers the type with TryAddScoped, but its dependencies — an HttpClient, IOptions<ClientCredentialsOptions>, IDistributedCache and an EcdsaTokenRefreshCoordinator — come from the hexasync.services.proxy package:

builder.Services.AddEcdsaInnerServiceResolver(builder.Configuration);
// env: SSO_SERVICE_URL, SSO_CLIENT_ID, SSO_CLIENT_SECRET

If the resolver cannot be obtained, capability resolution is skipped and the request proceeds with no capabilities.

Resolution flow

Driven by the system_role claim, then narrowed by the calling application:

Token type Claims Effective capabilities
User OAuth sub, client_id, system_role user caps ∩ app caps
Client credentials client_id, system_role app caps (from the app's role)
First-party SSO UI sub, client_id, system_role user caps — because that application declares no capabilities, not because the handler special-cases it

There is no first-party branch in the code. One rule decides all three rows: an application that declares capabilities narrows the caller to the intersection; one that declares nothing does not narrow (so the caller keeps its full user set); one whose capabilities cannot be resolved gets nothing (fail-closed); and a token carrying no client_id is granted nothing. See CapabilityIntersection and ApplicationCapabilityStatus for the exact rule.

Protecting endpoints

// Minimal API — OR (any of)
app.MapGet("/users", GetUsers).RequireCapability("users:read", "admin:full_access");

// Minimal API — AND (all of)
app.MapPost("/users", CreateUser).RequireAllCapabilities("users:write", "audit:enabled");

// Minimal API — require active sudo mode (re-verified identity).
// Otherwise 403 with body {"code":"SUDO_REQUIRED","message":"…"} — note the property is `code`, not `error`.
app.MapDelete("/users/{id}", DeleteUser).RequireSudo();

// MVC
[RequireCapability("users:read", "admin:full_access")]
public IActionResult GetUsers() { ... }

[RequireCapability("users:write", "audit:enabled", RequireAll = true)]
public IActionResult CreateUser() { ... }

Checking capabilities in code

using hexasync.auth.middleware.Capabilities;

if (httpContext.HasCapability(AppCapabilities.Sso.OAuthApps.Write)) { ... }

if (httpContext.HasAnyCapability(AppCapabilities.Sso.OAuthApps.Read, "admin:full_access")) { ... }

if (httpContext.HasAllCapabilities(AppCapabilities.Sso.OAuthApps.Read,
                                   AppCapabilities.Sso.OAuthApps.Write)) { ... }

IReadOnlyList<string> capabilities = httpContext.GetCapabilities();

AppCapabilities holds the constants — e.g. AppCapabilities.Sso.OAuthApps.Read is "identity:oauth_apps:read".

Other extensions

using hexasync.auth.middleware.Extensions;

Guid? userId   = httpContext.GetUserId();          // null for SERVICE tokens, or when no user id claim is present
Guid  required = httpContext.GetRequiredUserId();  // throws in those same cases
string? client = httpContext.GetClientId();

bool isService = httpContext.IsServiceToken();     // how you tell a client-credentials token from a user one
bool isUser    = httpContext.IsUserToken();

Guid? roleId = User.GetSystemRoleId();             // extension on ClaimsPrincipal, not HttpContext

RequireSudo() performs a claims-based check. If you register an ISudoValidationService, it is also consulted — but only when the claims check has already passed and the token carries both sudo_iat and sub in parseable form. A sudo token missing either skips the service entirely and is accepted on the claims check alone, so this is a server-side invalidation hook rather than an unconditional gate.

Security Considerations

  1. Use HTTPS for SsoUrl. RequireHttpsMetadata exists as an option but nothing enforces it, so this is on you.
  2. This package no longer accepts HMAC-signed tokens. SSO_SERVICE_SECRET is neither read nor bound here. It previously keyed HS* validation, which meant anyone holding that shared symmetric key could mint a token every service accepted — including one asserting an admin role. If your service still sets the variable for other purposes, that is fine; this package ignores it.
  3. Key rotation. JWKS keys are cached for JwksCacheDuration (24 hours by default, plus the 6-hour sliding expiry noted in the options table). A token bearing an unknown kid triggers one refresh and one retry, so a newly published key is picked up without waiting for the cache to expire.
  4. Clock skew. 5 minutes by default.

Troubleshooting

JWKS fetch fails — check connectivity to {SsoUrl}/.well-known/jwks.json, certificate validity, and firewall rules. On failure the provider returns an empty key set and validation fails closed.

Token validation fails — check expiry, issuer/audience against the defaults in the options table (they are constants, not derived from SsoUrl), and clock sync between services.

Options are unexpectedly at their defaults — you are probably on an Action<HexaSyncAuthOptions> overload, which does not read environment variables. See the note under Quick Start.

Capabilities are always empty — AddEcdsaInnerServiceResolver is probably not registered, or the token carries no system_role / client_id claim. A missing resolver and a missing system_role log at Debug; a token with no client_id, and a resolver that throws, log at Warning.

Unexplained 403s — subscribe to the meter hexasync.auth.capability (CapabilityTelemetry.MeterName); it exposes a hexasync.auth.capability.denied counter (CapabilityTelemetry.DeniedCounterName) incremented once per denial and tagged with reason (see CapabilityDenialReasons). Note the counter name is not the meter name — subscribing to hexasync.auth.capability.denied matches no meter and emits nothing, silently.

For a single request, CapabilityResolution.From(httpContext) returns the resolved set plus the ApplicationCapabilityStatus — or null when authentication did not run or resolution bailed out early, i.e. no system_role claim or no resolver registered. Those are the two Debug cases above, so a null here is itself the answer.

Note

CapabilityOptions (and the SSO_CAPABILITY_CACHE_MINUTES / SSO_REQUEST_TIMEOUT_SECONDS names in it) is vestigial — nothing binds it and nothing reads it. Setting those has no effect. The live cache and timeout knobs belong to ClientCredentialsOptions in hexasync.services.proxy.

License

MIT

No packages depend on hexasync.auth.middleware.

Version Downloads Last updated
2610.24.2 0 10/24/2026
2610.24.1 0 10/24/2026
2610.19.2 0 10/19/2026
2610.17.1-pre.3 1 10/17/2026
2610.17.1-pre.2 0 10/17/2026
2610.15.1 6 10/15/2026
2610.7.2 0 10/07/2026
2610.7.1 0 10/07/2026
2610.3.2 0 10/03/2026
2610.3.1 0 10/03/2026
2610.2.2 0 10/02/2026
2610.2.1 0 10/02/2026
2610.1.2 0 10/01/2026
2610.1.1 62 10/01/2026
2609.29.2 1 09/29/2026
2609.29.1 0 09/29/2026
2609.28.2 17 09/28/2026
2609.28.1 0 09/28/2026
2609.25.2 0 09/25/2026
2609.25.1 2 09/25/2026
2609.23.2 2 09/23/2026
2609.23.1 1 09/23/2026
2609.22.1 136 09/22/2026
2609.16.2 0 09/16/2026
2609.16.1 7 09/16/2026
2609.15.2 2 09/15/2026
2609.15.1 2 09/15/2026
2609.12.2 3 09/12/2026
2609.12.1 0 09/12/2026
2609.10.5 0 09/11/2026
2609.10.4 45 09/10/2026
2609.10.3 2 09/10/2026
2609.10.2 0 09/10/2026
2609.10.1 4 09/10/2026
2609.8.2 19 09/08/2026
2609.8.1 29 09/08/2026
2609.7.2 0 09/07/2026
2609.7.1 16 09/07/2026
2609.4.2 0 09/04/2026
2609.4.1 4 09/04/2026
2609.4.1-pre.2 0 09/04/2026
2609.4.1-pre.1 0 09/04/2026
2609.3.1-pre.2 0 09/03/2026
2609.3.1-pre.1 0 09/03/2026
2609.2.1-pre.3 0 09/02/2026
2609.2.1-pre.2 0 09/02/2026
2608.27.1-pre.2 0 08/27/2026
2607.23.1 287 07/22/2026
2607.22.2 0 07/22/2026
2607.22.1 52 07/22/2026
2607.21.1 0 07/21/2026
2607.16.3 0 07/16/2026
2607.16.2 0 07/16/2026
2607.16.1 0 07/16/2026
2607.15.1 0 07/15/2026
2607.14.1 25 07/14/2026
2607.13.1 184 07/13/2026
2607.11.1 0 07/11/2026
2607.8.1 0 07/08/2026
2607.5.1 13 07/05/2026
2607.4.1 0 07/04/2026
2606.30.1 82 06/30/2026
2606.29.1 0 06/29/2026
2606.27.1 14 06/27/2026
2606.26.6 1 06/26/2026
2606.26.5 0 06/26/2026
2606.26.3 50 06/26/2026
2606.26.1 4 06/25/2026
2606.25.1 0 06/25/2026
2606.23.1 1 06/23/2026
2606.22.6 20 06/22/2026
2606.22.5 34 06/22/2026
2606.22.4 7 06/22/2026
2606.22.1 10 06/21/2026
2606.19.1 9 06/19/2026
2606.17.1 0 06/17/2026
2606.16.3 0 06/14/2026
2606.16.1 0 06/16/2026
2606.14.3 5 06/14/2026
2606.14.2 2 06/14/2026
2606.12.5 28 06/12/2026
2605.28.2 101 05/28/2026
2605.28.1 0 05/28/2026
2605.26.3 13 05/26/2026
2605.26.2 6 05/26/2026
2605.26.1 0 05/26/2026
2605.25.1 0 05/25/2026
2605.23.3 7 05/23/2026
2605.23.2 2 05/23/2026
2605.23.1 0 05/23/2026
2605.22.1 0 05/22/2026
2605.19.1 10 05/19/2026
2605.18.2 154 05/18/2026
2605.18.1 0 05/18/2026
2605.16.3 0 05/16/2026
2604.27.4 39 04/27/2026
2604.27.3 1 04/27/2026
2604.27.2 1 04/27/2026
2604.12.1 179 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 147 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 77 03/19/2026
2603.19.1 0 03/19/2026
2603.18.1 46 03/18/2026
2603.16.3 223 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 2 03/14/2026
2601.15.1 53 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 57 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