feat(auth): deletion barrier at mint + fail-closed account fencing (slice 2/5 of #374) - #398
feat(auth): deletion barrier at mint + fail-closed account fencing (slice 2/5 of #374)#398lourou wants to merge 1 commit into
Conversation
…lice 2/5 of #374) Extracted verbatim from feature/delete-account-impl (#374): the DeletedIdentity barrier consulted at POST /v2/auth/token (terminal 410 identity_deleted after full SIWE validation, mint/teardown co-serialized on an identity advisory lock with an in-transaction barrier re-check), the deletion fence inside JWT auth itself (an accountId claim is honored only while the Account row exists, generic 401 otherwise, 500 on lookup failure, no positive caching, single DELETE /v2/accounts/me carve-out), async fail-closed requireAccount, the router-fencing audit over the real /v2 tree, and account-scoped agent-asset upload keys behind authOrAgentApiKeyAuth + requireAccount. Existing tests adapted (verbatim from #374) where fail-closed auth now requires the JWT's Account row to exist. The router-fencing test defers two cases to their slices: the subscription/claim surface (slice 5) and the DELETE /v2/accounts/me carve-out replay (slice 3). Deploy note: config.ts now refuses to start without DELETION_HASH_SECRET (>= 64 chars); set it in every environment before deploying this.
|
Warning Review limit reached
Next review available in: 10 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (23)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Claude finished @lourou's task —— View job Code Review CompleteThis is a well-architected implementation of account deletion defense mechanisms. The code demonstrates strong security engineering with fail-closed patterns, careful concurrency handling, and comprehensive testing. Below is detailed feedback organized by category. ✅ Security & CorrectnessExcellent implementation of fail-closed authentication (src/middleware/auth.ts:38-65) The deletion fence in
Strong concurrency control (src/accounts/repository.ts:31-38) The advisory lock pattern correctly serializes mint vs. deletion: await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${identityHash}, 0))`
HMAC barrier implementation is cryptographically sound (src/accounts/deletion/identity-hash.ts)
Router fencing audit is comprehensive (tests/deletion/router-fencing.test.ts) Two-layer audit approach is excellent:
🔍 Potential Issues1. Minor: Device backfill failure is swallowed silently (src/api/v2/auth/handlers/generate-token.ts:228-233) The } catch (err) {
req.log.warn({ err, deviceId: body.deviceId, accountId }, "auth.device.account_backfill_failed");
}Impact: Low - the backfill is truly optional and self-heals on next mint 2. Advisory: requireAccount performs duplicate lookup (src/middleware/auth.ts:245-284) Both Impact: Low - acceptable trade-off for fail-closed guarantees and middleware composability 3. Code Style: Inconsistent error response format Most errors return // generate-token.ts:121-124
res.status(410).json({
error: "This identity has been deleted",
code: "identity_deleted",
});Impact: None - this is actually good API design (structured error codes for client handling) ⚡ Performance Considerations1. No positive caching by design (documented in PR) Every authenticated request now performs an indexed PK lookup on Account. The PR description explicitly notes this. Analysis:
Recommendation: Monitor query latency metrics post-deploy. If this becomes a bottleneck at scale, consider:
2. Advisory lock on every SIWE mint (src/accounts/repository.ts:37) Mints are serialized per-identity via PostgreSQL advisory lock. Impact: Low - advisory locks are fast, and different identities don't block each other 🧪 Test CoverageExcellent coverage overall (1661/1661 passing) Specific strengths:
Test improvements to consider:
test("concurrent mint and deletion: barrier prevents resurrection", async () => {
// Two transactions racing: one minting, one deleting
// The advisory lock should prevent account resurrection
});
📋 Configuration & DeploymentEnvironment variable handling is excellent (src/config.ts:140-148) if (
!process.env.DELETION_HASH_SECRET ||
process.env.DELETION_HASH_SECRET.length < 64
) {
throw new Error(
"DELETION_HASH_SECRET is not configured or too short (need >= 64 chars / 32 bytes hex)",
);
}✅ Fail-fast at startup (not at first deletion) Deploy note is clear: PR description correctly emphasizes setting One suggestion: Consider adding a startup log line that confirms 🏗️ Code Quality & Best Practices1. Excellent documentation Every complex function has clear docstrings explaining:
2. Proper error handling hierarchy Custom error types ( 3. Strong separation of concerns
4. Type safety Good use of TypeScript, Zod schemas, and Prisma types throughout ✅ Minor suggestion: The // Current
const enforceLiveAccountClaim = async (...): Promise<boolean> => {
// Returns false after writing the response when the request must not proceed.
}
// Consider
const enforceLiveAccountClaim = async (...): Promise<{ shouldProceed: boolean }> => {
// More self-documenting
}🔐 Security-Specific Feedback1. No unauthenticated deletion oracle ✅ Barrier check only runs after full SIWE validation (generate-token.ts:115-116). Cannot probe for deleted accounts without valid signature. 2. Generic 401 prevents information leakage ✅ Deleted accounts get 3. Agent asset keys now account-scoped ✅ The S3 key pattern 4. Router fencing is auditable ✅ The source audit (router-fencing.test.ts:50-73) will fail CI if someone adds a new JWT verification call outside the fenced middlewares. This is excellent preventive security. 📝 Minor Code Style Notes1. Comment formatting is inconsistent Some files use 2. Magic numbers
🎯 Adherence to CLAUDE.md✅ No client-facing request schema changes: PR correctly notes the 410 is a new response for a state no shipped client can be in SummaryThis is high-quality, production-ready code. The implementation demonstrates:
No blocking issues. The minor observations above are optimizations and suggestions for future consideration, not bugs. Recommended next steps:
Overall assessment: LGTM 🚢 |
ApprovabilityVerdict: Needs human review This PR implements deletion barriers and fail-closed account fencing in core authentication middleware. Changes to auth/security infrastructure—including new HMAC-based identity barriers, transaction-level advisory locking, and database lookups on every authenticated request—require human review regardless of code ownership. You can customize Macroscope's approvability policy. Learn more. |
What this is
Slice 2 of 5 of #374, stacked on #397 (slice 1: data model). Content extracted verbatim from the #374 tip (45cd889); only surgical trims where a file mixes tiers.
This slice: the identity barrier and the fail-closed auth tier
src/accounts/deletion/barrier.ts,identity-hash.ts,src/accounts/repository.ts,generate-token.ts):POST /v2/auth/tokenconsults theDeletedIdentitykeyed-HMAC barrier only after full SIWE validation succeeds → terminal 410identity_deleted; mint and (future) teardown co-serialize onpg_advisory_xact_lockover the identity hash, with an in-transaction barrier re-check (IdentityBarredError), so a delete racing a mint can never recreate the account.src/middleware/auth.ts,require-live-account.ts): the fence lives inside JWT authentication itself — anaccountIdclaim is honored only while the Account row exists; deleted-account tokens get a generic 401 on every route (never a deletion-specific signal), infrastructure failure returns 500 (never a 401 that could make a client wipe its session);requireAccountis now async and fail-closed. Sole carve-out:DELETE /v2/accounts/me(method + path), for idempotent deletion-record replay once slice 3 lands.tests/deletion/router-fencing.test.ts): source audit (only the fenced middlewares may callverifyJwtToken) + behavioral audit of every JWT surface on the real/v2router. Two cases deferred to their slices, marked in-file: thesubscription/claimsurface (slice 5) and the DELETE carve-out replay test (slice 3).agent-assets.router.ts,get-presigned-url.ts, one-line mount swap insrc/api/v2/index.ts): presigned uploads now require a live account and producea/<accountId>/<uuid>object keys — the shape slice 3's S3 purge executor derives. JWT callers always use their own account; the trusted agent-key caller may assert an (existing) owner.No new migrations (slice 1 shipped the schema). No teardown endpoint, no tombstones, no claim, no outbox. No client-facing request-schema change (the 410 is a new response for a state no shipped client can be in until deletion ships; nothing for
assertLegacyShapeValidates). No credit movement.Deploy note (required env)
config.tsnow refuses to start withoutDELETION_HASH_SECRET(>= 64 chars,openssl rand -hex 32). Set it in every environment (dev + prod task definitions) before deploying this slice. Treat it as permanent: rotating it would orphan every barrier row. This is deliberately distinct from the freely-rotatable nonce secret.Note also: every authenticated request now performs one indexed PK lookup on Account (the fail-closed fence, no positive caching — by design, see #374's review rounds).
The series
ACCOUNT_DELETION_ENABLED=falseassertLegacyShapeValidatescontract pin)Validation
tsc --noEmit,eslint,prettier --checkall cleanNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Note
Add deletion barrier at account mint and fail-closed account fencing to auth middleware
DeletedIdentitybarrier:upsertAuthMethodAndAccountin repository.ts acquires a per-identity advisory lock and checks the barrier before creating accounts, throwingIdentityBarredErrorif the identity was previously deleted.generateTokenhandler in generate-token.ts pre-checks the deletion barrier and mapsIdentityBarredErrorto a410 identity_deletedresponse, preventing account recreation after deletion.authMiddleware,authMiddlewareAllowNSE,requireAccount) now performs a live database lookup on the account row for every JWT-authenticated request, returning401if the account no longer exists (fail-closed fencing).DELETION_HASH_SECRET(≥64 hex chars) is now required at startup; identity and account hashes are computed via HMAC-SHA256 in identity-hash.ts./api/v2/agents/assetspresigned URL route now scopes S3 object keys per-account (a/{accountId}/{uuid}) and accepts either a JWT or agent API key.401across all routes (exceptDELETE /v2/accounts/mefor idempotent retries).Macroscope summarized c5c852d.