fix(security): first-party JwtBearer with HS256 pinned; /auth/token mints for a cookie session only

The hand-rolled bearer handler is replaced by AddJwtBearer (Microsoft.AspNetCore.Authentication.JwtBearer
10.0.7, pinned on the same servicing line as the other AspNetCore packages). Validation parameters come
from JwtTokenService.BuildValidationParameters, which now pins ValidAlgorithms = HS256 for both the
bearer scheme and TryValidate; MapInboundClaims is off so the minted Role/zb:username keys resolve.
/auth/token requires the cookie scheme, so a bearer can no longer mint a fresh token for itself.

Refs #733
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 13:35:54 -04:00
parent 4d17b346c6
commit 3ddb6876db
10 changed files with 187 additions and 126 deletions
@@ -24,6 +24,17 @@ public static class AuthEndpoints
/// <summary>Response for a token issue request.</summary>
public sealed record TokenResponse(string Token);
/// <summary>
/// The <c>/auth/token</c> policy: an authenticated principal from the COOKIE scheme. Naming the scheme
/// makes the authorization middleware authenticate that scheme alone, so an <c>Authorization: Bearer</c>
/// header is never consulted here (with login disabled the auto-login handler sits under the same
/// scheme name, so the dev rig is unchanged).
/// </summary>
internal static readonly AuthorizationPolicy CookieSessionOnly =
new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
/// <summary>Maps OtOpcUa authentication endpoints to the application route builder.</summary>
/// <param name="app">The endpoint route builder.</param>
/// <returns>The endpoint route builder for chaining.</returns>
@@ -40,7 +51,10 @@ public static class AuthEndpoints
// Mapped only when a signing key is configured (JwtOptions.IsConfigured): an unconfigured
// endpoint used to answer HTTP 500 from the JwtTokenService constructor on every call.
if (app.ServiceProvider.GetRequiredService<IOptions<JwtOptions>>().Value.IsConfigured)
app.MapPost("/auth/token", (Delegate)IssueToken).RequireAuthorization();
// COOKIE scheme only (fix round 1 / I2): the default scheme also accepts a bearer, and a bearer
// that could mint a fresh token would renew itself forever — outliving its own expiry and the
// user's LDAP membership. A token is minted only for an interactive (cookie) session.
app.MapPost("/auth/token", (Delegate)IssueToken).RequireAuthorization(CookieSessionOnly);
else
app.ServiceProvider.GetService<ILoggerFactory>()?
.CreateLogger("ZB.MOM.WW.OtOpcUa.Security.AuthEndpoints")