refactor(auth): make step-up-without-Agent-binding unrepresentable (HT-75) - #97
Conversation
…T-75) Codex review of PR #96, finding 4 — the one item that PR deliberately left open. VerifyAuthenticationCeremonyParams was a single shape with an optional requireAgentId, so `{ ceremony: 'step-up' }` with no Agent type-checked fine. That combination silently disables BOTH halves of the binding added in #96 — the pre-consume check and consumeChallenge's AND agent_id = $3 — reopening the challenge-burn DoS. No caller does it; nothing but this type stopped one from starting. Now a discriminated union: 'step-up' requires requireAgentId, and 'authentication' forbids it via `requireAgentId?: never` (its challenges are minted with agent_id IS NULL, so binding one would match no row and break every login). No caller changes were needed — both existing call sites already passed the correct shape, which is the evidence that this tightens the contract without narrowing real behavior. Verified with a negative type test: both `step-up` without requireAgentId and `authentication` with it are now compile errors (TS2322). Gates: typecheck, lint, test:coverage all exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthrough
ChangesAuthentication ceremony parameter typing
Estimated code review effort: 2 (Simple) | ~5 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/auth/webauthn-ceremony.ts`:
- Around line 98-104: Enable exactOptionalPropertyTypes in both checked-in
TypeScript configurations, then add a negative type test covering `{ ceremony:
'authentication', requireAgentId: undefined }` against
VerifyAuthenticationCeremonyParams. Keep the authentication branch’s
requireAgentId?: never restriction unchanged and ensure the test verifies
explicit undefined is rejected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 193f1ae2-5f70-4c86-9ad3-50ecebf75e67
📒 Files selected for processing (1)
src/auth/webauthn-ceremony.ts
| export type VerifyAuthenticationCeremonyParams = VerifyAuthenticationCeremonyParamsBase & | ||
| ( | ||
| | { | ||
| /** Checked against the token's OWN `ceremony` field before anything else runs (spec §7's application-level discriminator check). */ | ||
| ceremony: 'authentication' | ||
| requireAgentId?: never | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'exactOptionalPropertyTypes' --glob 'tsconfig*.json' --glob '*.json' .
rg -n 'requireAgentId' --glob '*test*.ts' --glob '*test*.tsx' .Repository: Helpthread/helpthread
Length of output: 159
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== tsconfig files ==\n'
git ls-files 'tsconfig*.json' '**/tsconfig*.json'
printf '\n== exactOptionalPropertyTypes occurrences ==\n'
rg -n 'exactOptionalPropertyTypes' --glob 'tsconfig*.json' --glob '**/tsconfig*.json' --glob '*.json' .
printf '\n== webauthn ceremony file map ==\n'
ast-grep outline src/auth/webauthn-ceremony.ts --view expanded || true
printf '\n== requireAgentId test/type-test occurrences ==\n'
rg -n 'requireAgentId' --glob '*test*.ts' --glob '*test*.tsx' --glob '*.ts' --glob '*.tsx' src test tests .
printf '\n== nearby source context ==\n'
sed -n '1,220p' src/auth/webauthn-ceremony.tsRepository: Helpthread/helpthread
Length of output: 259
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== tsconfig.json ==\n'
cat -n tsconfig.json
printf '\n== web/tsconfig.json ==\n'
cat -n web/tsconfig.json
printf '\n== src/auth/webauthn-ceremony.ts (1-180) ==\n'
sed -n '1,180p' src/auth/webauthn-ceremony.ts
printf '\n== requireAgentId mentions in repo ==\n'
rg -n 'requireAgentId' src web . --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**'Repository: Helpthread/helpthread
Length of output: 14190
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== type-test / expect-error occurrences ==\n'
rg -n '`@ts-expect-error`|tsd|expectType|satisfies VerifyAuthenticationCeremonyParams|VerifyAuthenticationCeremonyParams' src tests .
printf '\n== src/auth/webauthn-ceremony.test.ts (160-260) ==\n'
sed -n '160,260p' src/auth/webauthn-ceremony.test.ts
printf '\n== package.json ==\n'
cat -n package.jsonRepository: Helpthread/helpthread
Length of output: 6840
Tighten the authentication branch’s requireAgentId restriction at src/auth/webauthn-ceremony.ts:98-104
requireAgentId?: never still accepts { ceremony: 'authentication', requireAgentId: undefined } under the current TypeScript config, since neither tsconfig.json nor web/tsconfig.json enables exactOptionalPropertyTypes. Turn that on for the checked-in configs and add a negative type test for the explicit undefined case.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/auth/webauthn-ceremony.ts` around lines 98 - 104, Enable
exactOptionalPropertyTypes in both checked-in TypeScript configurations, then
add a negative type test covering `{ ceremony: 'authentication', requireAgentId:
undefined }` against VerifyAuthenticationCeremonyParams. Keep the authentication
branch’s requireAgentId?: never restriction unchanged and ensure the test
verifies explicit undefined is rejected.
|
Adjudicated: valid observation, skipping the fix. Verified the property that actually matters —
The residual case CodeRabbit flags — Not enabling |
Closes the one item #96 deliberately left open — Codex review finding 4.
The footgun
VerifyAuthenticationCeremonyParamswas one shape with an optionalrequireAgentId, so this type-checked:That combination silently disables both halves of the Agent binding added in #96 — the pre-consume check and
consumeChallenge'sAND agent_id = $3— reopening the challenge-burn DoS. No caller did it. Nothing but this type stopped one from starting.The change
A discriminated union:
step-uprequiresrequireAgentId;authenticationforbids it (requireAgentId?: never), since login challenges are minted withagent_id IS NULLand binding one would match no row, breaking every login.No caller changes were needed. Both existing call sites already passed the correct shape — that's the evidence this tightens the contract without narrowing real behavior.
Verification
Negative type test — both forbidden combinations are now compile errors:
Gates:
typecheck,lint,test:coverageall exit 0.🤖 Generated with Claude Code
Summary by CodeRabbit