8.0 KiB
Admin API authentication
Implemented for GET /api/admin/whoami and the read-only suggestions API. The shared guard is apps/web/src/lib/auth/admin-api-auth.ts. This is not browser token login, a general admin mutation API, or the Velocity admission credential mechanism. Browser pages, privileged server actions, RCON, and Velocity authentication are unchanged. OpenAPI publication is separate work (US-026).
Credential selection
- No Authorization header: use the existing NextAuth administrator session and its configured role check. Player sessions do not qualify. Existing browser realm/client role behavior remains intact.
- Any Authorization header supplied: exclusively use bearer authentication. Empty, malformed, duplicated/combined, unsupported, expired, or otherwise invalid credentials never fall back to a browser session, including a privileged session cookie.
- The bearer scheme is case-insensitive. Send one compact signed JWT, not a client secret, bot token, or refresh token.
Machine verification
The web application directly depends on jose 6. Verification uses real cryptographic signature checking, not decoded-token role extraction:
- Only RS256 is accepted.
issmust exactly equal the trimmedKEYCLOAK_ISSUER_URLconfiguration.audmust containKEYCLOAK_CLIENT_ID(a string or audience array is supported).azpdoes not substitute foraud.expand a nonblank stringsubare required. Expired tokens and futurenbfare rejected with no added clock tolerance.- The required role (configured
KEYCLOAK_REQUIRED_ROLE, defaultminecraft-account-manager-admin) must appear inresource_access[KEYCLOAK_CLIENT_ID].roles. Realm roles or roles for other clients are not accepted. - The configured issuer must be HTTPS with no embedded credentials, query, or fragment. Keys come only from
<issuer-without-final-slash>/protocol/openid-connect/certs; tokeniss,jku, andx5unever select a key URL. Redirects are not followed.
One bounded, process-local remote JWKS resolver caches keys for ten minutes, coalesces concurrent fetches, permits refresh for unknown keys after a 30-second cooldown, and bounds each network fetch to five seconds. A changed configured issuer replaces the resolver. Replicas do not share the cache. Key rotation can temporarily reject a new key during cooldown; removed keys may remain usable until the cache refreshes. Access-token validity is local JWT verification, not per-request revocation/introspection. Use suitably short token lifetimes and synchronized clocks.
KEYCLOAK_CLIENT_SECRET is not needed for machine verification. This implementation does not obtain tokens or alter identity-provider clients, role/audience mappers, credentials, or deployments. See OIDC setup for the distinct browser configuration.
Safe identity endpoint
GET /api/admin/whoami authenticates and checks administrator permission before returning JSON with Cache-Control: no-store:
{
"authenticationMethod": "bearer",
"subject": "machine-subject",
"name": null,
"email": null
}
For browser sessions, authenticationMethod is session, subject is null (the existing session does not expose it), and name/email are the existing session values or null. Machine profile claims are not returned. No raw token, role list, session expiry, key material, client secret, or arbitrary claims are exposed. This endpoint requires no Discord or database access.
Failures
All guard failures use RFC 9457 application/problem+json, Cache-Control: no-store, a matching HTTP/body status, and a request-path instance. There are no sign-in redirects or token/error-detail logs.
| Status | Type | Meaning |
|---|---|---|
| 401 | urn:error:unauthorized |
Missing session or invalid supplied credentials; includes WWW-Authenticate: Bearer realm="admin-api". |
| 403 | urn:error:forbidden |
Authenticated identity lacks the required permission. |
| 503 | urn:error:admin-auth-unavailable |
Invalid/missing machine configuration, JWKS transport/format failure, or browser session service failure. |
Authentication is checked before suggestions cache access or Discord requests. Valid machine credentials do not enable writes: suggestions write methods remain 405. Errors do not reveal credentials, raw claims, upstream response bodies, or exception messages.
Verification and rollout boundary
Focused coverage lives in:
apps/web/src/lib/auth/admin-api-auth.test.ts: real signed JWTs, controlled JWKS HTTP transport (not mockedjwtVerify), validation failures, client-role isolation, configuration/network safety, caching, concurrent reads, and rotation.apps/web/src/app/api/admin/whoami/route.test.ts: safe identity projection and guard failures through the route.apps/web/src/app/api/suggestions/{route,machine-auth}.test.ts: browser regression, bearer precedence, all three read routes, cache authorization, and read-only behavior.
Run from the source repository:
npm test --workspace @minecraft-account-manager/web -- src/lib/auth/admin-api-auth.test.ts src/app/api/admin/whoami/route.test.ts src/app/api/suggestions
npm test
npm run lint
npm run typecheck
npm run build
npm run velocity:build
Test-first implementation evidence (US-025)
The following runs were observed against the local implementation; no commit or publication is part of this task:
Slice / focused test arguments after npm test --workspace @minecraft-account-manager/web -- |
Red before implementation | Green after implementation |
|---|---|---|
src/app/api/suggestions/route.test.ts |
6 failures: supplied headers fell through to the browser path (503 instead of 401), and missing-session 401 lacked the challenge. | 21 passing after shared-guard integration. |
src/lib/auth/admin-api-auth.test.ts |
15 failures: valid signed tokens were rejected; client-role and unavailable-service responses were not implemented. | 53 passing with the suggestions regression suite after actual JWT/JWKS verification. |
src/app/api/admin/whoami/route.test.ts |
6 explicit route-absence assertion failures (suite ran without a broken module import). | 59 passing across all three suites after adding the route; route discovery then refactored to a direct import. |
Additional integration/resilience regression coverage brought the focused suite to 72 passing tests: every suggestions read route, authorization before cached reads, unchanged write denial, untrusted signing keys, algorithm restrictions, concurrent JWKS fetching, key rotation, malformed upstream responses, and redirect refusal. Negative-token tests use an available privileged browser session to verify that invalid bearer credentials never fall back. The tests sign actual JWTs and exercise jose verification against controlled transport responses; jwtVerify is never mocked.
Full local verification passed: npm test (252 tests, including 214 web tests), npm run lint (zero errors; two existing navigation warnings in unchanged map-view-toggle.tsx), npm run typecheck, npm run build (whoami emitted as a dynamic route), and npm run velocity:build (clean test shadowJar, Java 17). The unrelated next-env.d.ts addition generated by Next.js during the build was removed to keep the source diff scoped.
Security scan: semgrep scan --config p/typescript --config p/jwt --metrics=off on the shared guard, suggestions wrapper, and whoami implementation completed with 74 rules, three files, zero findings. The initial --config auto --metrics=off invocation was rejected by Semgrep; the explicit-rule run is the successful result. This is scoped static-analysis evidence, not a complete security audit.
Offline tests do not establish live Keycloak audience/role issuance, JWKS reachability, or production authorization. Production deployment remains separately gated. After explicit release approval, verify machine whoami and suggestions reads, rejection of an unauthorized identity, and browser session access using approved credential handling (never token values in chat, command arguments, or logs).