fix(security): require auth on GET /api/v1/reauth and stop leaking SAT in body - #293
fix(security): require auth on GET /api/v1/reauth and stop leaking SAT in body#293birme 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 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.
birme
left a comment
There was a problem hiding this comment.
Code Review
Verdict: Needs Changes
Summary: The security fix itself is well-executed and faithfully mirrors the established requireWhipAuth pattern (constant-time timingSafeEqual, 401 + WWW-Authenticate, httpOnly cookie as the sole token delivery path, no raw SAT in the JSON body). However, this PR cannot merge as-is because it has a merge conflict with main, and there is one Blocking test-hygiene gap. Rebase and address the log mock before merge.
Blocking
- Merge conflict with
main—gh pr viewreportsmergeable=CONFLICTING,mergeStateStatus=DIRTY. The branch must be rebased/merged againstmainand conflicts resolved before this can land, independent of code quality. This is the primary blocker. src/api_re_auth.test.ts— The test file does not include the requiredjest.mock('./log', ...)mock mandated by the project testing rules for every backend test file. Per criteria, a test file missing this mock pollutes output and risks flaky timing behavior. Add:jest.mock('./log', () => ({ Log: () => ({ info: jest.fn(), error: jest.fn(), debug: jest.fn(), warn: jest.fn() }) }));
Warnings
src/api_re_auth.ts:24—requireReAuth(request: any, reply: any)uses bareanyfor both parameters without a justifying comment. This mirrors the existingrequireWhipAuthinapi_whip.ts:58, so it is a consistency-preserving copy rather than a new regression, but it defeats TypeScript on a security-critical path. PreferFastifyRequest/FastifyReply. (If left as-is to match WHIP, add a one-line comment noting the intentional parity.)src/server.ts:17-27— The new startup SECURITY warning (unauthenticated/reauth) has no test coverage. The PR adds a genuinely new behavior (env-driven warning, incl. the whitespace-only-key branch); an assertion that the warning fires whenOSC_ACCESS_TOKENis set but no effective key is present would guard this. Not a hard blocker, but recommended given this is the safety net for auth-off-by-default.
Suggestions
src/api_re_auth.ts:37-40— Thetoken.length === key.lengthshort-circuit beforetimingSafeEqualtechnically leaks key length via timing. This is a negligible, industry-standard tradeoff (and identical to the WHIP implementation), so it is fine to keep — noting only for completeness.src/api_re_auth.ts—requireReAuthisasyncbut performs no awaited work; it could be a synchronous helper. Minor; parity withrequireWhipAuthjustifies leaving it.- Test coverage is otherwise strong: 401 for missing/empty/malformed/prefix-of-key Bearer,
fetchnot called on reject, notokenin the 200 body, cookie still set, and the unauthenticated-when-no-key path. Good regression coverage for #264. (Verified the 200-path tests are valid becausejest.setup.jssetsOSC_ACCESS_TOKEN='foo'globally.)
Domain Note
This change touches the OSC service-access-token (SAT) reauth lifecycle. The auth mechanics are standard and low-risk, so no intercom-expert consult is required, but confirm downstream frontend callers of /api/v1/reauth no longer read token from the response body (they must now rely on the cookie).
CI is green (lint, prettier, typecheck, unittests all SUCCESS), but green CI does not clear the merge conflict.
Next steps: rebase onto
mainto clear the conflict, then pass Blocking items to bug-fixer → once resolved, use pr-author to update the PR.
birme
left a comment
There was a problem hiding this comment.
Code Review
Verdict: Needs Changes
Summary: The security fix itself is well-implemented — it closes #264 by removing the raw SAT from the JSON body and adds a constant-time Bearer auth guard that faithfully mirrors the established requireWhipAuth pattern, with solid regression tests. However, the PR is currently CONFLICTING with main and cannot merge until rebased. That merge conflict is the sole blocker; the remaining items are warnings/suggestions.
Blocking
- Merge conflict with base branch (
main) — GitHub reportsmergeable=CONFLICTING/mergeStateStatus=DIRTY. This must be resolved by rebasing the branch (264-reauth-endpoint-auth) onto the latestmainand pushing before the PR can merge. Please re-request review after the rebase so the final merged diff can be re-verified (in particularsrc/api.tsandsrc/server.ts, which are the likely conflict sites where the newreAuthKeyoption is threaded throughApiOptions).
Warnings
src/api_re_auth.test.ts— This test file does notjest.mock('./log', ...). Project convention requires every backend test file to mock./log. In practice this file does not currently exercise aLog()call path (that lives inserver.ts), and the omission is pre-existing rather than introduced by this PR, so it is not a blocker — but since you are already expanding this file substantially, adding the standard mock now would bring it into line with the rest of the suite and avoid future output pollution.src/api_re_auth.ts:23—requireReAuth(request: any, reply: any)usesanyfor both parameters. This is copied verbatim fromrequireWhipAuthinapi_whip.ts, so it is a consistent (not new) deviation, but theanycasts defeat type-checking on the request/reply. Consider typing these asFastifyRequest/FastifyReply. If you keepanyto stay symmetric withrequireWhipAuth, add a one-line comment noting the intentional parity.
Suggestions
src/api_re_auth.ts:32—request.headers['Authorization'](capitalized) is dead code: Node/Fastify normalize all incoming header names to lowercase, so this branch of the||never matches. It is harmless and mirrorsrequireWhipAuth, but the fallback could be dropped for clarity.src/api_re_auth.ts:22— Good call trimmingreAuthKey(opts.reAuthKey?.trim()) so a whitespace-only key is treated as unset; this correctly aligns with the whitespace-only branch of the startup warning inserver.ts. Worth a brief inline comment tying the two together.src/server.ts:17-27— TheSECURITY:startup warning is a nice defense-in-depth touch and correctly distinguishes theundefinedvs. whitespace-only cases. No change needed.- Tests — Coverage is strong: 401 on missing/empty/malformed/wrong Bearer, the proper-prefix timing case, the success path asserting
{ success: true }withtokenabsent and thesatcookie present, the unauthenticated-when-unconfigured path, and the 500 token-service-unavailable path.jest.setup.jssetsOSC_ACCESS_TOKEN='foo'globally, so the fetch-path tests correctly reach the token service rather than short-circuiting to 405. This exercises the exact failure path from #264.
Domain Note
This change touches the /reauth OSC service-access-token lifecycle. It does not alter WHIP/WHEP session handling or audio routing, so no intercom-expert consultation is required.
Next steps: resolve the Blocking merge conflict by rebasing on
main, then re-request review. The warning items can be passed to bug-fixer if you choose to address them.
|
daily-backlog-pr: code-reviewer verdict Needs Changes — the PR is currently CONFLICTING with |
Summary
requireReAuthguard toGET /api/v1/reauth(mirrorsrequireWhipAuth): Bearer header with constant-timetimingSafeEqual,401+WWW-Authenticate: Bearer realm="reauth". Auth is disabled when no key is configured so existing installs keep working.REAUTH_AUTH_KEY, falling back toWHIP_AUTH_KEY.{ ok: true }); thehttpOnlycookie remains the sole delivery path./reauthis effectively unauthenticated (incl. whitespace-only key), so auth-off-by-default is never silent.Test plan
npm test)npm run typecheck)npm run lint)GET /api/v1/reauthwith no/invalid Bearer returns 401 whenREAUTH_AUTH_KEY/WHIP_AUTH_KEYis settokenvalue;satcookie is still setCloses #264
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com