fix(security): require bearer auth on GET /api/v1/reauth (#264) - #283
fix(security): require bearer auth on GET /api/v1/reauth (#264)#283alexbj75 wants to merge 3 commits into
Conversation
The reauth endpoint was registered with a schema only - no preHandler, no onRequest, no auth - so any unauthenticated caller could mint a valid OSC service access token. - Add requireReAuth, mirroring requireWhipAuth in api_whip.ts: Bearer header, constant-time timingSafeEqual comparison, 401 + WWW-Authenticate: Bearer realm="reauth", and auth disabled when no key is configured (existing installations keep working). - Configure via REAUTH_AUTH_KEY, falling back to WHIP_AUTH_KEY. - Defense in depth: stop returning the token in the JSON response body; the httpOnly cookie remains the delivery path. Closes #264
QA-granskning — PR #283 (säkerhet: auth på
|
QA review of #283: auth-off-by-default is the right call for backwards compatibility, but it must not be silent. An install with OSC_ACCESS_TOKEN set and no effective key still hands out a service access token with no signal at all. A whitespace-only REAUTH_AUTH_KEY is worse: it looks configured but is falsy after trim, so auth is off while the operator believes it is on - the warning distinguishes that case as a configuration error. Also adds 401 coverage for empty Bearer, malformed header without the Bearer prefix, and a token that is a proper prefix of the key.
|
All APIs when running in an OSC context is behind the OSC managed auth wall so we should not add any bearer auth on the app level that will conflict with that. The reauth endpoint is only relevant when running in an OSC context and where this is needed |
Closes #264.
Problem
GET /api/v1/reauthwas registered insrc/api_re_auth.tswith aschemaonly — nopreHandler, noonRequest, no auth of any kind — andsrc/api.tsregistered the plugin without an auth hook. Any unauthenticated caller could therefore mint a valid OSC service access token for the instance (the global@fastify/rate-limitthrottles, but does not authenticate).Change
1. Auth on the route — mirroring the existing house pattern.
requireReAuthis a direct mirror ofrequireWhipAuthinsrc/api_whip.ts(lines 58–80):Authorization: Bearer …header, constant-time comparison viacrypto.timingSafeEqualwith an explicit length check,401withWWW-Authenticate: Bearer realm="reauth", charset="UTF-8", and auth disabled when no key is configured. No new dependency (timingSafeEqualis in Node core).2. Configuration — same route as
whipAuthKey.New
ApiReAuthOptions { reAuthKey?: string }, folded intoApiOptionsand passed at registration, exactly aswhipAuthKeyis.src/server.tsfeeds it fromprocess.env.REAUTH_AUTH_KEY ?? process.env.WHIP_AUTH_KEY.The
WHIP_AUTH_KEYfallback is deliberate:WHIP_AUTH_KEYis currently the only bearer key this backend checks, andintercom-frontendalready sendsAuthorization: Bearer ${VITE_BACKEND_API_KEY}on every call includingreauth(src/api/api.ts:302–311). Existing deployments that setWHIP_AUTH_KEYtherefore get the endpoint protected with no config change and no client change;REAUTH_AUTH_KEYallows a separate key. Deployments with neither key set are unchanged (endpoint stays open) — documented inreadme.md.Point 2 — token in the response body
Removed. The handler now sends
{ success: true }; the token continues to be delivered as theeyevinn-intercom-manager.sathttpOnly cookie.ReAuthResponseinsrc/models.tsupdated accordingly.Evidence this breaks no consumer:
grep -ri reauth src/returns only the route itself, its registration inapi.ts:157, and the model.Eyevinn/intercom-frontend—API.reauth()is typedPromise<void>(src/api/api.ts:302) anduse-reauth.tsxdiscards the resolved value entirely; it relies on the cookie. The cookie ishttpOnly, so browser JS could never have read it from there anyway.This is nonetheless a breaking response-schema change for any out-of-tree client that reads
tokenfrom the body — call it out in release notes. Auth (point 1) is the actual fix; this is defense in depth (token out of logs, proxies, browser history).Not silent when disabled (QA review).
src/server.tslogs a warning at startup whenOSC_ACCESS_TOKENis set and the effective key is missing or whitespace-only:The two reasons are separated on purpose:
reAuthKey?.trim()makesREAUTH_AUTH_KEY=" "falsy, so a key that looks configured in an env file silently disables auth. That is a configuration error, not a choice.Deployment note
intercom-frontendsendsAuthorization: Bearer ${VITE_BACKEND_API_KEY}. If a key is configured on the backend, the frontend must be built with a matchingVITE_BACKEND_API_KEY— otherwisereauthstarts returning401for a frontend built without one.Tests
src/api_re_auth.test.ts— the token service is now mocked, so the suite no longer makes real network calls totoken.svc.*.osaas.io:401+WWW-Authenticate, no token minted, no cookie set401, no token mintedAuthorization: Bearer) →401Bearerprefix →401401(length check short-circuitstimingSafeEqual)200, cookie set, body is{ success: true }with notoken200(existing installations are not broken)500(regression cover for the retry path)Verification
npm test— 14 suites, 250 tests, all passnpm run typecheck— cleannpm run lint— 0 errors (280 pre-existing warnings, unchanged)prettierappliedNot touched:
infra/terraform/aws/, dependencies, any other route.