Identity, OAuth and the account lifecycle
Read references/IDENTITY-AND-ACCOUNTS.md before applying any of this.
That file is the standard; everything below it is a summary to help you decide
whether this skill applies and to check your work afterwards.
Reference-architecture principles: P5.
What this standard covers
- Claims are enriched at issuance
- Refresh tokens
- OAuth providers
- Account linking
- Enumeration safety
- Lockout
- Transactional email
- Account deletion
- Versioned legal consent
- Key material
Failure modes
| Symptom | Cause |
|---|---|
Provider error redirect_uri_mismatch |
Callback built from an internal Request.Host; set the public callback base URL |
| OAuth callback 404s | Provider registered with the controller's route instead of the middleware's /signin-* path |
| Two accounts for one person | Link-by-email flow missing; first external sign-in created a duplicate |
| Password reset reveals which emails exist | Branches return different messages/timing; unify to the generic 200 |
| Sessions survive password reset | Refresh revocation not wired to the reset path |
| Support tickets: "locked out forever" | Lockout shipped without the admin unlock endpoint |
| Invitation email storms | No attempt log/cooldown; resend button wired straight to the sender |
| "Deleted" user's files still downloadable | Blob cleanup missing from the reaper; cascade never reaches object storage |
| Users never see updated terms | Version bumped in the document but not in configuration; /me never flags it |
| Dead provider buttons in the UI | Frontend hardcodes providers instead of reading the discovery endpoint |
openssl: command not found on somebody's first day |
Setup runbook assumes a Unix shell; no path given for the platform half the team is on |
| PEM rejected as unreadable | Newlines arrived escaped as literal \n through an environment variable or secret store |
| Every consumer rejects every token, but nothing is unhealthy | Key never reached the service; the algorithm was inferred as symmetric and the JWKS is a valid, empty key set |
| Connection string breaks one rotation after it was fine | Generated password contained +, /, = or ; and was never escaped |
| A rotation signed everybody out | Retired public key dropped from the validation set immediately instead of after one token lifetime |
Checklist
- Authorization facts (tier, workspace roles) stamped into claims at issuance; no downstream callback
- Refresh tokens: CSPRNG, single-use rotation, stored hashed, global revocation invoked from logout/reset/deletion
- OAuth: middleware callback paths; explicit public callback base URL;
returnUrlin state, validated against an allowlist; provider discovery endpoint - All four account-linking flows implemented; link-by-email notifies the user
- Forgot/reset enumeration-safe with a test per branch; OAuth-only behaviour decided and recorded
- Lockout enabled, with an admin unlock endpoint
- Email inventory documented incl. deliberate silences; attempt log + escalating cooldowns; no-op provider fallback
- Soft delete + retention + reaper + admin restore; typed confirmation + password; blob cleanup explicit
- Consent versions in config; immutable acceptance audit rows; cookie consent default-deny
- Signing keys generated rather than invented; private half held only by the identity service;
kidderived from the key - Rotation is rolling — retired public key stays in the validation set and the JWKS for one token lifetime
- Key generation works on every platform contributors actually use, and each command is copy-pasteable on its own line
- Local development keys are distinct from deployed ones
- The deploy asserts the published key set is non-empty
Generated from docs/guides/IDENTITY-AND-ACCOUNTS.md by scripts/build-marketplace.mjs. Do not edit this file: change the source document, or its entry in catalog/marketplace.catalog.json, and re-run the generator.