feat(auth): passkeys (WebAuthn) + cross-deployment auto-login suggestions - #323
feat(auth): passkeys (WebAuthn) + cross-deployment auto-login suggestions#323lopugit wants to merge 11 commits into
Conversation
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36346992 | Triggered | Generic Password | 9e541d6 | remix/app/api/utils/auth/passkeyAaguids.ts | View secret |
| 36346991 | Triggered | Generic Password | 9e541d6 | remix/app/api/utils/auth/passkeyAaguids.ts | View secret |
| 36346992 | Triggered | Generic Password | c2687e9 | remix/app/api/utils/auth/passkeyAaguids.ts | View secret |
| 36346991 | Triggered | Generic Password | c2687e9 | remix/app/api/utils/auth/passkeyAaguids.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Develop S3 preview ready
The alias passed the develop bucket CORS preflight and a final live PR/SHA fence. Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup. |
|
|
…off, FedCM IdP Extends #323's auto-login beyond the *.thingtime.com cookie family while honoring the owner's constraint end to end: quick-login is powered by the sessions in the USER'S BROWSER, federated per deployment — never a central session store. thingtime.com is only a broker reading the browser's own tt_hints cookie and its own switcher roster. Layer 1 — federated hint resolution (multi-database environments): account-hints reports foreign-DB origins as 'unresolved'; the new GET /api/v1/auth/account-hints/resolve (CORS-credentialed, Thingtime family only, read-only) lets each environment vouch for exactly the pointers its origin wrote; useAccountHints fans out (cap 4) and merges by user id. The browser assembles the full picture. Layer 2 — cross-origin session handoff: POST /auth/sso-handoff mints a 2-minute, aud-bound, single-use code (purpose-fenced JWT backed by a pre-minted session that self-expires unclaimed); the receiving deployment's POST /auth/sso-session verifies signature + aud === its own public origin, claims atomically once (replay revokes — theft signal), then runs the exact password-login tail. Different-environment redemption fails closed. UI: /authorize?self=1 popup (embedded login with hints strip when signed out, "Continue to <host>?" card when signed in) and a "Sign in with Thingtime" card on foreign origins. Layer 3 — FedCM IdP: /.well-known/web-identity + config/accounts/ client-metadata/assertion endpoints; Sec-Fetch-Dest: webidentity enforced; accounts = this browser's own roster (anti-fixation gate intact — only redeemable sessions are offered); assertion mints handoff codes for thingtime-self or baseline-profile app tokens for registered clients; the foreign-origin card prefers the native sheet via IdentityCredential feature-detect. Verified: scripts/verify-federated-login.mjs 31/31 against two stacks on SEPARATE mongods (recipe + its two sharp edges documented in the script header: stack B must be a production build — nitro dev dotenv re-overrides .env on config reloads — and a second mongod, because the home db name is pinned to 'thingtime'). verify-passkeys.mjs still 44/44. Browser-verified both /authorize?self=1 states. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Auto-rebase running — the stack rebase started working on this PR at 09:17 UTC. Expected to finish around 09:32 UTC (most rebases still land in 5-15 minutes; the hosted-runner ceiling is 360). On success this branch is force-pushed onto its new base and a result comment follows — no manual action is needed meanwhile. |
…ions
Passkeys, everything-is-a-thing edition:
- protected 'passkey' things: credential material in the secure blob,
counter as root secureCounter, metadata in crystal, uniqueness +
login lookup via uniqueKeys 'passkeyCredential:<id>' (BinData);
written only by auth/passkeys.ts through the HOME things collection
- 'passkey-app-link' child things record where each passkey signs in
(origin always, registered SSO clientId additionally), upserted via
the new crystal.linkKey partial unique index
- ceremonies: stateless purpose-fenced JWT challenge cookies (10 min,
cleared on verify); registration is password-confirmed and requests
discoverable credentials (residentKey required) so usernameless
login + conditional-UI autofill (iCloud Keychain / 1Password popups
via autocomplete="username webauthn") work; login requires user
verification, checks revocation before any cryptography, finishes
through the same roster-merge tail as password login, and bypasses
email-OTP 2FA by design (sessions carry meta.method 'passkey')
- rpID is thingtime.com across every *.thingtime.com deployment (one
passkey works on prod, dev, and previews), exact hostname elsewhere
- management: GET list (linked apps aggregated one-query-per-kind),
update (nickname/description), password-confirmed revoke (immediate
permanent block) and delete (revoke-first, frees the authenticator)
Cross-deployment auto-login:
- every sign-in path (login, register, passkey, assume, temporary)
rides mergeAccountSession, which now appends a {rosterId, origin}
pointer to the Domain=.thingtime.com tt_hints cookie (api/cookies.ts
gained the domain option); pointers only — never identities/tokens
- GET /api/v1/auth/account-hints resolves pointers LIVE through
getLiveRosterEntries + resolveSessionUser (the switcher's own
chokepoints): suggestions exist exactly while the remote session is
live, dead pointers self-prune, responses are a slim projection
(no email), same-origin only, rate-limited
- UI: root-mounted AutoLoginPopup (corner card, 24h snooze, hidden on
auth pages), login-form suggestion strip + /login?u= prefill,
Settings → Security passkey manager — all localCache-seeded
Fixed during browser verification:
- useApi identity churn made the hints/list effects request-loop until
the rate limiter 429'd — apiRef idiom (useAccountSwitcher's) applied
- behind the Vite dev proxy (changeOrigin) request.url derived
rpID 127.0.0.1 / internal hint origins, which a real browser
ceremony could never verify — resolvePublicOrigin now honors
x-forwarded-host/-proto (same trust stance as isSameOriginPost)
Verified: remix/scripts/verify-passkeys.mjs — a pure-Node software
WebAuthn authenticator (P-256, minimal CBOR, none attestation, DER
ES256) driving the real API end to end, 42/42; test:schemas 55/55
(projection pin updated); browser-verified desktop + mobile (popup,
prefill, manager rename/revoke flows).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hen PR #320 lands Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…false-positiving GitGuardian's generic-password detector flags '<uuid>': '<name>' object entries beside password-manager names as hardcoded credentials on every commit touching the file (these are PUBLIC authenticator identifiers). Tuple list + Map carries identical data without the assignment shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rm in embedded login The conditional-UI ceremony armed on the login form stayed pending after login/navigation — a stale WebAuthn request is what lets the browser's cross-device QR sheet (or a password-manager popup) surface uninvited later. Cancel it in the effect cleanup (login finished, 2FA step, unmount), and don't arm background autofill in the embedded Add-account form at all (it renders while already signed in; the modal button remains). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…complete Browsers report a failed cross-device (QR/Bluetooth) handoff with the same NotAllowedError as a user cancel, so the button flows were silent exactly when the user scanned a QR and nothing happened. Explicit clicks now always toast (gentle info for cancel-ish outcomes, error otherwise); only the background conditional-autofill arm stays silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yments) A deployment may prune only hint pointers its OWN origin wrote: a foreign *.thingtime.com pointer that doesn't resolve locally is indistinguishable between 'session ended' and 'that environment runs a different database' (branch-scoped DBs), so visiting one environment must never destroy another environment's hints. Unresolvable foreign pointers are kept (never displayed — only verified-live accounts render); the cookie's newest-first cap retires genuinely dead ones. E2E grows to 44 checks pinning prune-own/keep-foreign. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refreshed by the rebase-pr-stacks workflow: https://github.com/lopugit/thingtime/actions/runs/32236799115
3227349 to
18b6c56
Compare
|
🧬 Rebased
The branch was published once with an exact force-with-lease. Please review the rewritten commits before relying on them. |
…off, FedCM IdP Extends #323's auto-login beyond the *.thingtime.com cookie family while honoring the owner's constraint end to end: quick-login is powered by the sessions in the USER'S BROWSER, federated per deployment — never a central session store. thingtime.com is only a broker reading the browser's own tt_hints cookie and its own switcher roster. Layer 1 — federated hint resolution (multi-database environments): account-hints reports foreign-DB origins as 'unresolved'; the new GET /api/v1/auth/account-hints/resolve (CORS-credentialed, Thingtime family only, read-only) lets each environment vouch for exactly the pointers its origin wrote; useAccountHints fans out (cap 4) and merges by user id. The browser assembles the full picture. Layer 2 — cross-origin session handoff: POST /auth/sso-handoff mints a 2-minute, aud-bound, single-use code (purpose-fenced JWT backed by a pre-minted session that self-expires unclaimed); the receiving deployment's POST /auth/sso-session verifies signature + aud === its own public origin, claims atomically once (replay revokes — theft signal), then runs the exact password-login tail. Different-environment redemption fails closed. UI: /authorize?self=1 popup (embedded login with hints strip when signed out, "Continue to <host>?" card when signed in) and a "Sign in with Thingtime" card on foreign origins. Layer 3 — FedCM IdP: /.well-known/web-identity + config/accounts/ client-metadata/assertion endpoints; Sec-Fetch-Dest: webidentity enforced; accounts = this browser's own roster (anti-fixation gate intact — only redeemable sessions are offered); assertion mints handoff codes for thingtime-self or baseline-profile app tokens for registered clients; the foreign-origin card prefers the native sheet via IdentityCredential feature-detect. Verified: scripts/verify-federated-login.mjs 31/31 against two stacks on SEPARATE mongods (recipe + its two sharp edges documented in the script header: stack B must be a production build — nitro dev dotenv re-overrides .env on config reloads — and a second mongod, because the home db name is pinned to 'thingtime'). verify-passkeys.mjs still 44/44. Browser-verified both /authorize?self=1 states. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…off, FedCM IdP Extends #323's auto-login beyond the *.thingtime.com cookie family while honoring the owner's constraint end to end: quick-login is powered by the sessions in the USER'S BROWSER, federated per deployment — never a central session store. thingtime.com is only a broker reading the browser's own tt_hints cookie and its own switcher roster. Layer 1 — federated hint resolution (multi-database environments): account-hints reports foreign-DB origins as 'unresolved'; the new GET /api/v1/auth/account-hints/resolve (CORS-credentialed, Thingtime family only, read-only) lets each environment vouch for exactly the pointers its origin wrote; useAccountHints fans out (cap 4) and merges by user id. The browser assembles the full picture. Layer 2 — cross-origin session handoff: POST /auth/sso-handoff mints a 2-minute, aud-bound, single-use code (purpose-fenced JWT backed by a pre-minted session that self-expires unclaimed); the receiving deployment's POST /auth/sso-session verifies signature + aud === its own public origin, claims atomically once (replay revokes — theft signal), then runs the exact password-login tail. Different-environment redemption fails closed. UI: /authorize?self=1 popup (embedded login with hints strip when signed out, "Continue to <host>?" card when signed in) and a "Sign in with Thingtime" card on foreign origins. Layer 3 — FedCM IdP: /.well-known/web-identity + config/accounts/ client-metadata/assertion endpoints; Sec-Fetch-Dest: webidentity enforced; accounts = this browser's own roster (anti-fixation gate intact — only redeemable sessions are offered); assertion mints handoff codes for thingtime-self or baseline-profile app tokens for registered clients; the foreign-origin card prefers the native sheet via IdentityCredential feature-detect. Verified: scripts/verify-federated-login.mjs 31/31 against two stacks on SEPARATE mongods (recipe + its two sharp edges documented in the script header: stack B must be a production build — nitro dev dotenv re-overrides .env on config reloads — and a second mongod, because the home db name is pinned to 'thingtime'). verify-passkeys.mjs still 44/44. Browser-verified both /authorize?self=1 states. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…psed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On non-*.thingtime.com deployments the browser's native 'Continue as' sheet now auto-invokes on load (FedCM's designed usage — the browser applies its own dismissal cooldowns), making the auto-login popup work in browser chrome on any domain; the manual card stays as the fallback for unsupported browsers, cooldowns, and no-account states. The SSO hub is overridable via localStorage tt-sso-hub so foreign-origin flows are testable against a preview alias before the hub code reaches production thingtime.com. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two features, one auth-family PR — full details in PRs/passkeys-and-auto-login.md.
🔑 Passkeys (WebAuthn)
residentKey: required) — the platform sheet offers Save to iCloud Keychain / 1Password / security key natively. Provider names derive from authenticator AAGUIDs.autocomplete="username webauthn"+ conditional mediation) so Safari/Chrome/1Password surface their passkey popup right on the username field. Bypasses email-OTP 2FA by design; finishes through the exact password-login tail (roster merge, other accounts untouched); sessions carrymeta.method: "passkey".thingtime.comacross*.thingtime.com(prod, dev, previews).passkey/passkey-app-linkthings — credential material in thesecureblob,uniqueKeyscredential-id uniqueness, HOME collection only (att_mongooverride can never capture or plant credentials). Stateless purpose-fenced JWT challenge cookies, cleared on verify (no replay).✨ Cross-deployment auto-login ("Continue as…")
{rosterId, origin}pointer to aDomain=.thingtime.comtt_hintscookie — never identities or tokens.GET /api/v1/auth/account-hintsresolves pointers live through the switcher's own roster/session chokepoints: log out elsewhere and the suggestion disappears here; dead pointers self-prune; responses carry no email; same-origin only./login?u=(password re-entry deliberate) or runs the passkey ceremony. 24h snooze, never on auth pages. The roster anti-fixation ownership gate is untouched.🧪 Verification
remix/scripts/verify-passkeys.mjs— 42/42: a pure-Node software WebAuthn authenticator (P-256, minimal CBOR,noneattestation, DER ES256) drives the real API end to end — attestation verify, duplicate 409, challenge-replay refusals, usernameless login, lastUsed + linked apps, revocation blocking login, revoke-before-delete, pointer-only hint resolution, logout-elsewhere hint death, docs endpoints.test:schemas55/55 (projection pin updated),test:collections, auth unit tests, ESLint clean.🐛 Bugs found & fixed during verification
useApi()identity churn request-looped the hints fetch until the rate limiter 429'd (limiter did its job) — fixed with theapiRefidiom.request.urlderivedrpID: 127.0.0.1/ internal hint origins — real browser ceremonies could never verify.resolvePublicOriginnow honorsx-forwarded-host/-proto(the proxy already sends them, PR MongoDB data-endpoint config: session override + saved endpoints (thin-frontend mode) #84).TESTING.md gained a passkeys + auto-login checklist;
remix/CHANGELOG.mdupdated.🤖 Generated with Claude Code
🌐 Also included: Login with Thingtime anywhere (collapsed from #327)
Extends the auto-login beyond the
*.thingtime.comcookie family — browser-held sessions, federated per deployment, never a central session store. Full note: PRs/login-with-thingtime-anywhere.md./account-hintsreports foreign-database origins asunresolved; each origin's newGET /account-hints/resolve(family-only CORS, credentialed, read-only) vouches for exactly the pointers it wrote; the client fans out (cap 4) and merges. The browser assembles the cross-environment picture.POST /auth/sso-handoffmints a 2-minute, aud-bound, single-use code; the receiving deployment'sPOST /auth/sso-sessionverifies signature + own-origin aud, claims atomically once (replay revokes — theft signal), then runs the password-login tail. Different-environment redemption fails closed. Driven by the/authorize?self=1popup ("Continue to ?") and a "Sign in with Thingtime 🌈" card on foreign origins (immutable*.vercel.apppreviews)./.well-known/web-identity+ config/accounts/client-metadata/assertion;Sec-Fetch-Dest: webidentityenforced; accounts = this browser's own switcher roster only; assertions mint handoff codes (thingtime-self) or baseline-profile app tokens (registered clients). Foreign-origin card prefers the native sheet, popup fallback.Verification:
verify-federated-login.mjs31/31 against two stacks on separate mongods (recipe + its sharp edges in the script header);verify-passkeys.mjs44/44 re-run after the collapse.