Concierge.Auth.Client.AuthGuard
2.0.0
See the version list below for details.
dotnet add package Concierge.Auth.Client.AuthGuard --version 2.0.0
NuGet\Install-Package Concierge.Auth.Client.AuthGuard -Version 2.0.0
<PackageReference Include="Concierge.Auth.Client.AuthGuard" Version="2.0.0" />
<PackageVersion Include="Concierge.Auth.Client.AuthGuard" Version="2.0.0" />
<PackageReference Include="Concierge.Auth.Client.AuthGuard" />
paket add Concierge.Auth.Client.AuthGuard --version 2.0.0
#r "nuget: Concierge.Auth.Client.AuthGuard, 2.0.0"
#:package Concierge.Auth.Client.AuthGuard@2.0.0
#addin nuget:?package=Concierge.Auth.Client.AuthGuard&version=2.0.0
#tool nuget:?package=Concierge.Auth.Client.AuthGuard&version=2.0.0
Concierge.Auth.Client.AuthGuard
Two ASP.NET Core authentication schemes: ConciergeJwt verifies AuthService-issued human JWTs on
incoming requests (contract §5) and WebSocket handshakes (§7); a named API-key scheme validates a
presented key against a host-supplied loader. The only Concierge.Auth.Client.* package that
references ASP.NET Core (FrameworkReference, never a Microsoft.AspNetCore.* PackageReference)
or StackExchange.Redis.
The two-step check, in this exact order (contract §5)
- Signature/JWKS. RS256 only, via
Concierge.Auth.Client.Keys'sIJwksKeyProvider— this package never fetches JWKS itself.ClockSkewis hardcoded to 5 seconds (contract §5 item 3); ASP.NET's own default is 5 minutes, and that default is never reachable here — there is no option to change it.JsonWebTokenHandler, not the legacyJwtSecurityTokenHandler. Issuer and audience validation are off, because the token carries noiss/audclaim to validate against (contract §3.2: exactly{sub, jti, iat, exp}) — there is nothing to check. jtideny-list — MANDATORY, and only ever reached if step 1 already succeeded.EXISTS denylist:jti:<jti>inredis-cache(contract §3.4). Present → rejected. There is no in-memory fallback and no option to skip this check. If Redis is unreachable at request time, the request is rejected (fail closed) — never treated as "not denied". An in-memory alternative across multiple instances would silently fail to log a user out everywhere but one instance, which is worse than no deny-list at all because it looks like it works.
On success, exactly { userId, jti } (ConciergeRequestContext) is attached — never roles,
scopes, permissions, tenant, email, or displayName. That is PolicyService's data, resolved
separately from the cached assignment set (contract §3.2; integration-guide invariant 11). This
package has no authorization opinion and no PolicyService dependency.
Install and register
services.AddConciergeAuthClient(configuration, db => db.UseNpgsql(cs, npgsql =>
npgsql.MigrationsHistoryTable("__EFMigrationsHistory", "concierge"))); // base package — required first
services.AddConciergeAuthClientKeys(configuration); // Keys package — supplies IJwksKeyProvider
services
.AddConciergeAuthGuard() // required first call — wires up ASP.NET Core authentication
.AddConciergeJwtBearer(configuration); // binds "Concierge:AuthClient:TokenValidation"
Fails closed at this call, synchronously (decision D8): if
ConciergeTokenValidationOptions.RedisConnectionString is not configured,
AddConciergeJwtBearer throws OptionsValidationException immediately — an operator sees the
misconfiguration at registration time, not on the first request that needed the deny-list. There
is no way to configure this package without a deny-list store.
// appsettings.json
{
"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 — see
"Multiple schemes" below to target one explicitly. On success, ConciergeJwt attaches
{ userId, jti }; read it downstream with:
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
AddConciergeJwtBearer and AddConciergeApiKey are independent — register any combination.
Whichever call runs first becomes AuthenticationOptions.DefaultScheme, plain
registration-order, no priority flag:
services
.AddConciergeAuthGuard()
.AddConciergeJwtBearer(configuration) // default, since it's first
.AddConciergeApiKey("partnerA", o => o.SecretLoader = ...)
.AddConciergeApiKey("partnerB", o => o.SecretLoader = ...);
Target a specific scheme with [Authorize(AuthenticationSchemes = "partnerA")]; a custom scheme
name is available for the JWT scheme too (AddConciergeJwtBearer(configuration, "MyJwtScheme")).
WebSocket handshake (contract §7)
The token arrives explicitly in the handshake payload (auth: { token }), not a header — extract
it with whatever WebSocket library the host uses, then:
var result = await webSocketAuthenticator.AuthenticateHandshakeAsync(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);
RunUntilExpiryAsync closes the socket once the token's exp passes — a long-lived connection
must not outlive the credential that opened it.
Failure codes
TOKEN_INVALID— malformed, wrong key, wrong/disallowed algorithm (includingnoneand HS256), or expired/not-yet-valid (outside the 5s clock skew).TOKEN_DENIED— signature was valid, but thejtiis on the deny-list.DENYLIST_UNAVAILABLE— the deny-list store could not be reached; the request is 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).
| 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.0)
- Concierge.Auth.Client.Keys (>= 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.