Concierge.Auth.Client.AuthGuard
3.1.1
dotnet add package Concierge.Auth.Client.AuthGuard --version 3.1.1
NuGet\Install-Package Concierge.Auth.Client.AuthGuard -Version 3.1.1
<PackageReference Include="Concierge.Auth.Client.AuthGuard" Version="3.1.1" />
<PackageVersion Include="Concierge.Auth.Client.AuthGuard" Version="3.1.1" />
<PackageReference Include="Concierge.Auth.Client.AuthGuard" />
paket add Concierge.Auth.Client.AuthGuard --version 3.1.1
#r "nuget: Concierge.Auth.Client.AuthGuard, 3.1.1"
#:package Concierge.Auth.Client.AuthGuard@3.1.1
#addin nuget:?package=Concierge.Auth.Client.AuthGuard&version=3.1.1
#tool nuget:?package=Concierge.Auth.Client.AuthGuard&version=3.1.1
Concierge.Auth.Client.AuthGuard
Three ASP.NET Core authentication schemes: ConciergeSession validates opaque session tokens
(contract §11.7); ConciergeJwt validates machine client-credential JWTs (contract §11.3, RS256/JWKS
via Concierge.Auth.Client.Keys's IJwksKeyProvider, then mandatory Redis checks); and a named
API-key scheme validates a presented key against a host-supplied loader. WebSocket handshake
support covers both the session lane and the JWT lane (contract §7 / §11.9). The only Concierge.Auth.Client.*
package that references ASP.NET Core (FrameworkReference, never a Microsoft.AspNetCore.*
PackageReference) or StackExchange.Redis.
Session lane (AddConciergeSessionToken) — contract §11.6–§11.7
- Extract the opaque token —
Authorization: Beareror the configured httpOnly cookie, perTransport. GET sess:token:<sha256(token)>onredis-cache— one round trip on cache hit; no HTTP.- Hit → check
idleExpiresAt/absoluteExpiresAt; if cookie transport + unsafe method, comparesha256(X-CSRF-Token)to the payload'scsrfHash(bound tosessionId). - Miss →
POST /client-services/v1/:clientKey/sessions/validatewith this integration's §10 credential; on success repopulate the cache (TTL =min(idleRemaining, absoluteRemaining, 900)s, floored at 1 — matches AuthServiceSessionService.CacheTtlCapSeconds, not configurable). - Miss + unknown/revoked/expired → write negative cache (
"0",NegativeCacheSeconds, default 30 s) and reject. - Redis unreachable →
503+Retry-After: 5. Never authenticate. - On success attach
{ userId, sessionId }(ConciergeRequestContext) — never roles, scopes, permissions, tenant, email, or displayName.
Fails closed at registration, synchronously: RedisConnectionString, AuthServiceBaseUrl,
ClientKey, and ClientSecret are all required (OptionsValidationException per property).
Transport=Cookie with EnableCsrfProtection=false is refused — same rule as contract §11.5.
services
.AddConciergeAuthGuard()
.AddConciergeSessionToken(configuration) // scheme "ConciergeSession" — humans
.AddConciergeJwtBearer(configuration); // scheme "ConciergeJwt" — machines
// appsettings.json — SessionValidation section
{
"Concierge": {
"AuthClient": {
"SessionValidation": {
"RedisConnectionString": "localhost:6379",
"AuthServiceBaseUrl": "https://auth.example",
"ClientKey": "my-service",
"ClientSecret": "<from credential store>",
"Transport": "Bearer",
"CookieName": "concierge_session_at",
"EnableCsrfProtection": false,
"NegativeCacheSeconds": 30
}
}
}
}
Target a scheme explicitly when registering more than one:
[Authorize(AuthenticationSchemes = ConciergeSessionDefaults.AuthenticationScheme)].
JWT lane (AddConciergeJwtBearer) — contract §5 / §11.3
- Signature/JWKS. RS256 only, via
IJwksKeyProvider.ClockSkewis hardcoded to 5 seconds.JsonWebTokenHandler, not the legacyJwtSecurityTokenHandler. typguard (§11.3). TheConciergeJwtscheme requirestyp: "client"; any token with notypis rejected. The WebSocket / user-context path (ValidateAsync) rejects any payloadtypclaim, includingtyp: "client".client-revoked-at:<sub>epoch (T-0132). Client-credential tokens whoseiatpredates the Redis epoch are rejected; Redis unreachable → fail closed (same posture as the deny-list).jtideny-list — MANDATORY after the above succeed.EXISTS denylist:jti:<jti>.
On JWT success, { sub, jti } is attached as { userId, jti }. On session success,
{ userId, sessionId }.
Install and register (JWT + API key)
services.AddConciergeAuthClient(configuration, db => db.UseNpgsql(cs, npgsql =>
npgsql.MigrationsHistoryTable("__EFMigrationsHistory", "concierge"))); // base — required first
services.AddConciergeAuthClientKeys(configuration); // Keys — supplies IJwksKeyProvider
services
.AddConciergeAuthGuard()
.AddConciergeJwtBearer(configuration); // binds "Concierge:AuthClient:TokenValidation"
Fails closed at this call, synchronously (decision D8): if
ConciergeTokenValidationOptions.RedisConnectionString is not configured,
AddConciergeJwtBearer throws OptionsValidationException immediately.
// appsettings.json — TokenValidation section
{
"Concierge": {
"AuthClient": {
"TokenValidation": {
"RedisConnectionString": "localhost:6379",
"RedisDatabase": 0
}
}
}
}
Standard ASP.NET Core pipeline — no custom UseX call:
app.UseAuthentication();
app.UseAuthorization();
[Authorize] (no scheme specified) targets whichever scheme was registered first. Read attached
context downstream:
ConciergeRequestContext? ctx = httpContext.GetConciergeRequestContext();
API key scheme
AddConciergeApiKey(schemeName, configure) registers one independent scheme per call — one per
partner/integration, for example. It ships no default credential source: configure MUST set
SecretLoader, and a scheme registered without one fails at options resolution
(OptionsValidationException) — fail-closed, same contract as the JWT scheme's Redis check.
services.AddSingleton<IConciergeCredentialCache, MyRedisCredentialCache>(); // host-supplied — see below
services
.AddConciergeAuthGuard()
.AddConciergeApiKey("partnerA", options =>
{
options.HeaderName = "X-Api-Key"; // default
options.SecretLoader = (sp, ct) =>
sp.GetRequiredService<IClientCredentialStore>().GetActiveSecretAsync(ct);
});
The resolved secret is cached under the scheme name ("partnerA") in IConciergeCredentialCache
so SecretLoader is not called on every request; CacheTtl (default 5 minutes) is advisory.
Comparison against the presented header value is constant-time.
IConciergeCredentialCache — host-supplied, no default shipped
Defined in the base package (Concierge.Auth.Client.Caching). This package ships no
implementation — in-memory, Redis, a database, whatever fits — the same provider-agnosticism the
base package already applies to EF Core:
public interface IConciergeCredentialCache
{
Task<string?> GetAsync(string key, CancellationToken cancellationToken);
Task SetAsync(string key, string value, TimeSpan? ttl, CancellationToken cancellationToken);
}
Multiple schemes
AddConciergeSessionToken, AddConciergeJwtBearer, and AddConciergeApiKey are independent —
register any combination. Whichever call runs first becomes AuthenticationOptions.DefaultScheme,
plain registration-order, no priority flag:
services
.AddConciergeAuthGuard()
.AddConciergeSessionToken(configuration) // default when first
.AddConciergeJwtBearer(configuration)
.AddConciergeApiKey("partnerA", o => o.SecretLoader = ...);
Target a specific scheme with [Authorize(AuthenticationSchemes = "partnerA")]; custom scheme names
are available on session and JWT registration overloads too.
WebSocket handshake (contract §7 / §11.9)
The token arrives explicitly in the handshake payload (auth: { token }), not a header — extract
it with whatever WebSocket library the host uses, then:
// Human lane — opaque session token OR user-context JWT (no typ claim):
var result = await webSocketAuthenticator.AuthenticateHandshakeAsync(token, cancellationToken);
// Machine lane — client-credential JWT (typ: "client"):
var clientResult = await webSocketAuthenticator.AuthenticateClientHandshakeAsync(token, cancellationToken);
if (result.IsFailure)
{
// reject the handshake — same failure semantics as the HTTP path
}
// after accepting the socket:
await webSocketAuthenticator.RunUntilExpiryAsync(socket, result.Value.ExpiresAt, cancellationToken);
AuthenticateHandshakeAsync routes opaque tokens through ISessionValidator (same cache-hit →
validate-fallback → negative-cache sequence as HTTP) and JWT-shaped tokens through
IConciergeTokenValidator in user-context mode. AuthenticateClientHandshakeAsync uses
ValidateClientTokenAsync, including the typ guard and client-revocation epoch.
RunUntilExpiryAsync closes the socket once the credential's expiry passes — a long-lived connection
must not outlive the credential that opened it. For sessions, ExpiresAt is the earlier of
idleExpiresAt and absoluteExpiresAt from the live payload.
Failure codes
Session lane
SESSION_INVALID— unknown, revoked, expired, reused, or negative-cached session.CSRF_TOKEN_INVALID— cookie transport, unsafe method, missing/wrong CSRF (403).SESSION_STORE_UNAVAILABLE— Redis unreachable (503 +Retry-After: 5).
JWT lane
TOKEN_INVALID— malformed, wrong key, wrong/disallowed algorithm, expired, wrong/missingtyp.TOKEN_DENIED— signature valid butjtideny-listed oriatbefore client-revocation epoch.DENYLIST_UNAVAILABLE— deny-list or client-revocation store unreachable; request rejected.
Out of scope
Any authorization decision, any PolicyService call, profile operations, credential rotation (see
Concierge.Auth.Client.Secrets), JWKS fetching (see Concierge.Auth.Client.Keys), obtaining
session tokens (see Concierge.Auth.Client.Sessions).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Concierge.Auth.Client (>= 1.1.1)
- Concierge.Auth.Client.Keys (>= 2.1.0)
- Concierge.Auth.Client.PeerSignature (>= 1.0.0)
- Microsoft.IdentityModel.JsonWebTokens (>= 8.22.0)
- Microsoft.IdentityModel.Tokens (>= 8.22.0)
- StackExchange.Redis (>= 2.8.22)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.