diff --git a/Distribution/docs/authentication_flow.md b/Distribution/docs/authentication_flow.md new file mode 100644 index 0000000..ecddb8c --- /dev/null +++ b/Distribution/docs/authentication_flow.md @@ -0,0 +1,102 @@ +# Authentication — Own-User Auth via External Identity Service (AuthHex) + +**Status**: Done +**Depends On**: — (cross-cutting infrastructure, not a numbered feature step) + +--- + +## Goal + +Own-user (staff/API) authentication is delegated to an external identity service ("AuthHex"), +which owns credentials and signs RS256 JWTs. This app validates those tokens and maps them onto +its own `User`/`Role` tables so the existing permission-policy authorization stays unchanged. + +--- + +## Background / Context + +Previously this app verified passwords and minted its own HMAC-signed JWT locally. That's been +replaced — AuthHex now owns credentials and signing; this app only holds AuthHex's **public** key +to validate tokens, and never sees a plaintext password for this flow. + +Authorization is untouched: permission policies, role levels, scope checks, and the seeded role +set all still work exactly as before, because every validated token is mapped back onto a local +`User`/`Role` row before any authorization check runs. + +--- + +## Configuration Changes + +```jsonc +"Jwt": { + "PublicKey": "...", + "Audience": "AuthHexClient", + "Issuer": "AuthHex" +}, +"AuthHex": { "BaseUrl": "http://localhost:6102" } +``` + +`Jwt:Key` (old symmetric secret) is removed. `Jwt:PublicKey` is AuthHex's RSA public key in XML +format (not PEM). + +--- + +## API Endpoints + +| Method | Route | Notes | +|---|---|---| +| POST | /api/auth/login | Proxies to AuthHex `loginUser` | +| POST | /api/auth/register | Proxies to AuthHex `registerUser` | +| GET | /api/auth/me | `[Authorize]`, proxies to AuthHex `getUserDetails` | +| POST | /api/auth/logout | `[Authorize]`, proxies to AuthHex `LogoutUser` | + +All four just forward to `POST {AuthHex:BaseUrl}/api/user` and pass AuthHex's response back +as-is. No cookie — token returned in the response body, sent back as `Authorization: Bearer`. + +--- + +## Business Rules + +1. No local password check or token issuance — both delegated to AuthHex. +2. `AddJwtBearer` validates issuer/audience/signature/lifetime against AuthHex's public key. +3. `OnTokenValidated` maps the token's `sub` claim to a local `User` (`IUserRepository`); fails + the token if no matching active user exists. +4. On success, `username`/`role_id`/`role_level` claims are attached from the **local** `User`/ + `Role` row — authorization checks downstream are unaffected by anything AuthHex's own token + claims contain. +5. Only one validation mechanism runs (`AddJwtBearer`) — two ported alternatives exist but are not + registered (see Implementation Notes). +6. No refresh tokens — once the AuthHex-issued token expires, the client must log in again. + +--- + +## Permissions to Add to Seed + +None — this only changes authentication. Permission policies and role seed data are untouched. + +--- + +## Open Questions + +- Add a refresh-token flow if AuthHex doesn't offer one and re-login UX proves disruptive. +- Keep or delete the ported `AuthMiddleware`/`JwtMiddleware` if never actually needed. +- No retry/circuit-breaker if AuthHex is unreachable. + +--- + +## Implementation Notes + +``` +Features/Auth/ — AuthController, AuthService, AuthDTOs (thin AuthHex proxies) +Infra/Authorization/ + AuthHex/ — IAuthHexClient, AuthHexClient, AuthHexResponse + Jwt/RsaXmlKeyReader.cs — portable XML→RSA key parser (RSA.FromXmlString is Windows-only) + Middleware/ — AuthMiddleware, JwtMiddleware, JwtOptions, ApiResponse + (ported from OSMCore for reference — NOT registered) +Program.cs — AddHttpClient, AddJwtBearer + OnTokenValidated + (maps token → local User/Role → role_id/role_level claims) +``` + +**Gotcha:** don't run more than one of `AddJwtBearer` / `AuthMiddleware` / +`JwtMiddleware.CustomMiddlewareAttribute` at once — each re-validates the same token +independently.