hexasync.auth.middleware 2610.24.2
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
Environment variables (recommended)
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(viaAddHttpClient, so the concrete registration is transient)ITokenValidator→EcdsaTokenValidator(singleton)IEcdsaInnerServiceTokenResolver→StandardEcdsaInnerServiceTokenResolver(scoped,TryAdd)IMemoryCache- The
HexaSyncauthentication 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
- Use HTTPS for
SsoUrl.RequireHttpsMetadataexists as an option but nothing enforces it, so this is on you. - This package no longer accepts HMAC-signed tokens.
SSO_SERVICE_SECRETis neither read nor bound here. It previously keyedHS*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. - 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 unknownkidtriggers one refresh and one retry, so a newly published key is picked up without waiting for the cache to expire. - 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.
.NET 10.0
- Dapper (>= 2.1.86)
- MessagePack (>= 3.1.9)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.12)
- Microsoft.AspNetCore.Mvc.Abstractions (>= 2.3.13)
- Microsoft.Extensions.Http (>= 10.0.12)
- Snappier (>= 1.3.1)
- StackExchange.Redis (>= 3.1.13)
- hexasync.services.proxy (>= 1.10.66)
| 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 |