AyCode.Core/docs/adr/0001-user-bearer-token-flow.md

111 lines
8.9 KiB
Markdown

# ADR 0001: User bearer-token authentication flow (HTTP + SignalR + tag dispatch)
## Status
Accepted (2026-04-25)
## Context
The framework has partial JWT infrastructure on the server side (`AcLoginServiceServer` produces access + refresh tokens via standard `JwtSecurityTokenHandler`; `IAcLoggedInModelBase.AccessToken` returns the token to the client) but no end-to-end bearer-token flow:
- Client-side login service is not implemented (all methods throw `NotImplementedException`).
- HTTP transport does not inject the bearer header on outgoing calls.
- SignalR transport has no access-token provider wired into the hub-connection builder.
- Server has no `AddJwtBearer` registration, no `[Authorize]` plumbing, and no per-tag authorization for the single-method `OnReceiveMessage(int tag, ...)` SignalR dispatch.
Two security defects in `AcLoginServiceServer.GenerateAccessToken` (JWT signing key + issued access token written to logger) were addressed pre-flight as `LOG-I-9` and `LOG-I-10` `Closed`. Their existence is the proximate trigger for this ADR — they exposed the absence of an end-to-end auth contract.
Consumer apps (MAUI + WASM) cannot adopt the framework safely until this ADR's decisions are implemented.
**Cross-project scope:** AyCode.Services + AyCode.Services.Server + AyCode.Interfaces + AyCode.Models.Server + every consumer. ADR placed at repo-root `docs/adr/` per the multi-project routing rule in [`.github/skills/adr-author/SKILL.md`](../../.github/skills/adr-author/SKILL.md) Step 1 ("cross-cutting decision → highest common ancestor").
**In scope:** HTTP transport, SignalR transport, per-tag SignalR dispatch authorization, both client (MAUI + WASM) and server.
**Out of scope (deferred to future ADRs or TODO):**
- Refresh-token flow design (sliding lifetime, proactive vs reactive refresh, server-side token store) — separate ADR `0002`.
- Role-based / claims-based authorization beyond authenticated / anonymous.
- Multi-tenant claim shape.
- OAuth2 / external-IdP integration.
- Token revocation / blacklist.
- Logout server-side invalidation (currently client-side discard only).
## Decision
Adopt a layered bearer-token authentication architecture across all client and server entry points. The architectural choices:
1. **Token storage is abstracted at the framework layer.** Consumers supply platform-specific implementations (MAUI: secure storage, WASM: protected browser storage). Framework provides the contract and a default in-memory implementation for tests / fallback.
2. **HTTP transport binding is automatic.** A framework-provided delegating handler reads from the token store and injects the bearer header on every outgoing call. Consumers register one named HttpClient configuration; per-call header injection is eliminated.
3. **SignalR transport binding is automatic.** A framework-provided hub-builder extension wires the access-token provider from the token store. Browser-hosted clients (WASM) use the standard query-string token-transport fallback because the WebSocket API cannot carry custom headers; server accepts both header and query-string forms transparently for `/hub`-prefixed paths.
4. **Server validates JWTs via the standard ASP.NET Core authentication pipeline.** A framework DI bundle binds JWT options from consumer configuration and registers JwtBearer authentication, including hub-aware events that extract the query-string token for SignalR connections.
5. **Per-tag SignalR dispatch is gated by an authorization attribute.** The hub class requires authentication by default; specific tags (login, ping, public broadcast) are exempted via an explicit allowlist attribute on the hub. The dispatcher checks authorization before resolving the target method for a given tag, keeping the existing `[Tag(N)]` registry as the single source of truth (no parallel policy registry).
6. **Token lifetime is 24 hours, regenerated fresh on each login.** Lifetime is configurable per consumer; refresh-flow design is deferred to ADR `0002`. When the access token expires the user re-authenticates (no mid-session refresh in this ADR's scope).
7. **Security hardening is built into the framework setup.** JWT options are validated at startup (key length ≥ 256 bits for HS256; issuer + audience non-empty); HTTPS metadata is required outside `Development`. Pre-existing log-leak defects were addressed pre-flight (see Consequences → Pre-flight done).
## Consequences
**Positive:**
- Consumers get bearer-auth via three DI calls (one for JWT bundle, one for HTTP-handler registration, one for hub-builder extension) — minimum-boilerplate target met.
- Per-tag authorization scales with the existing tag registry — no parallel registry to maintain.
- Two confirmed-critical security leaks (`LOG-I-9`, `LOG-I-10`) are closed; future similar mistakes prevented by the framework guideline doc (`docs/AUTH/README.md`'s "Never log secrets" section).
- WASM and MAUI use one shared token-store abstraction; consumer code is platform-aware only at registration time.
- Framework-first preserved: all architectural pieces live in AyCode.Services / AyCode.Services.Server / AyCode.Interfaces; consumers supply only platform-storage and config values.
**Negative:**
- 24-hour fixed lifetime forces re-authentication at expiry; user experience friction until ADR `0002` (refresh) lands.
- Query-string token in WASM SignalR connections → access logs must redact `access_token` URL parameters (consumer Kestrel/proxy config concern; documented in `docs/AUTH/README.md`).
- Existing consumer projects must add the new DI calls and the per-hub allowlist attribute — breaking change for any consumer currently relying on an unauthenticated hub.
**Follow-ups required:**
*Pre-flight (already landed in this commit series):*
-`LOG-I-9` closed: JWT signing key log-write DEBUG-gated.
-`LOG-I-10` closed: access token log-write DEBUG-gated; `AccesToken` typo fixed at the same line.
*Infrastructure (one-time, before implementation):*
- Create `docs/AUTH/` topic folder with `README.md` (consumer recipe: class names, method signatures, wire formats, diagrams, code examples), `AUTH_ISSUES.md`, `AUTH_TODO.md`.
- Propose new topic code `AUTH` for the workspace registry (`TOPIC_CODES.md` row + Decision Log entry `LLMP-DEC-N`).
*Implementation (separate commit series, framework layer):*
- Token-store abstraction + in-memory default implementation.
- HTTP delegating handler + DI extension method bundling its registration with a named HttpClient.
- SignalR hub-builder extension (client side) + JWT DI bundle (server side) including hub-aware query-string token events.
- Authorization attribute + tag-allowlist attribute + dispatcher hook in the hub base.
- JWT options POCO + startup `IValidateOptions<T>` validator.
- Repo-wide grep follow-up: search for any other call sites that may log secrets in similar patterns (preventive).
*Consumer wiring (per-consumer projects, separate tasks):*
- MAUI token-store implementation (consumer side).
- WASM token-store implementation (consumer side).
- Per-consumer `appsettings.json` `AyCode:Jwt` section.
- Per-consumer hub: tag allowlist for login + ping (at minimum).
*Status migration on AUTH topic creation:*
- Move `LOG-I-9` and `LOG-I-10` to `AUTH_ISSUES.md` per their existing "Discovery context" notes; in `LOGGING_ISSUES.md` replace bodies with cross-ref `Superseded by AUTH-I-N`.
*Future ADRs:*
- `0002` — Refresh-token flow + sliding-vs-fixed lifetime decision.
- Possibly later: role/claims authorization, multi-tenant claims, OAuth2 / external-IdP (per the deferred OUT-of-scope items).
## Alternatives considered
- **In-memory-only token store** (rejected): app-restart re-login on MAUI is unacceptable UX.
- **Per-call manual `Authorization` header injection** (rejected): boilerplate + error-prone; framework should encapsulate.
- **Hub-class-only `[Authorize]` without per-tag allowlist** (rejected): login + ping tags would also require auth → bootstrap deadlock.
- **Per-tag DI policy registry** (rejected): too much complexity for Layer 0; mixes consumer-specific tag lists into framework.
- **Custom token middleware from scratch** (rejected): non-standard, no `[Authorize]` interop, no out-of-the-box JWT validation.
- *Future flexibility:* locks us out of standard OAuth2 / external-IdP integration when ADR `0002+` extends scope; the chosen `JwtBearer` pipeline keeps that door open.
- *Future flexibility:* locks out standard OAuth2 / external-IdP integration when ADR `0002+` extends scope; the chosen `JwtBearer` pipeline keeps that door open.
## Related
- Pre-flight fix entries: [`LOGGING_ISSUES.md#log-i-9`](../../AyCode.Core/docs/LOGGING/LOGGING_ISSUES.md), [`LOGGING_ISSUES.md#log-i-10`](../../AyCode.Core/docs/LOGGING/LOGGING_ISSUES.md) (both `Closed (2026-04-25)`).
- Tentative TODO surfaced post-fix: `LOG-T-12` ("Never log secrets" guideline section in `LOGGING/README.md`).
- External: RFC 6750 (Bearer Token Usage), RFC 7519 (JWT), Microsoft.AspNetCore.SignalR auth docs (query-string token pattern).
- Future: ADR `0002` (refresh-flow design).