4ce2df0a2b
CI / backend (push) Successful in 52s
CI / frontend (push) Successful in 14s
Deploy Staging / deploy (push) Successful in 18s
CI / backend (pull_request) Successful in 52s
CI / frontend (pull_request) Successful in 15s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 55s
Security / secrets (pull_request) Successful in 4s
Security / dependencies (pull_request) Successful in 54s
4.1 KiB
4.1 KiB
02 — Auth & Sign-in (Part 2)
OAuth is the login. No passwords. A user's identity is the set of provider accounts
linked to them; any one can authenticate the session. Identity key is (provider, sub)
— never email (emails change; sub is stable, and the same email can exist on Google and
Microsoft as distinct accounts).
Provider-selection sign-in (first-time)
[ Choose how to sign in ]
▸ Continue with Google ▸ Continue with Microsoft ( ▸ IMAP — future )
- User picks a provider → redirect to provider OAuth (PKCE,
state,nonce). - Callback → exchange code →
GetIdentityAsyncreturns(provider, sub, email, name). - Resolve: look up
accounts (provider, sub).- No match → first-time. Create
user(first user ever = Admin, otherwiseMemberifsystem_settings.registration_open, else reject) +account(is_login_identity=true)- encrypted
provider_tokens. Start session.
- encrypted
- Match → existing user. Refresh tokens, start session.
- No match → first-time. Create
- Kick off the account's initial sync.
Adding another account later (linking) — the security-critical flow
The user is already authenticated. "Add account" → provider OAuth in link mode:
- Callback identity
(provider, sub):- Unlinked → attach a new
account(mailbox) to the current user. ✅ - Already linked to this user → no-op / "already connected."
- Already linked to another user → blocked by the unique
(provider, provider_account_id)constraint + explicit check → error "This mailbox is connected to a different InboxIntel user." This is the anti-hijack guarantee — you can only link an identity you can authenticate and that no one else owns.
- Unlinked → attach a new
- A single user can hold N accounts across Google/Microsoft/IMAP; each can also serve as a login identity (any of them signs you into the same user).
Switching
- Switch mailbox (same user): an account switcher changes the active mailbox context (or "All accounts" unified view). No re-auth — it's all one user. Search can scope to one account or span all.
- Switch user (different person): full sign-out → sign-in. Optional "fast switch" could hold multiple sessions, but for a small team, explicit re-auth is simplest and safest.
Sessions
- Opaque server-side session (
sessionstable) referenced by an HttpOnly · Secure · SameSite=Lax cookie. Provider tokens are never exposed to the browser. - Rotate session id on login (anti-fixation); idle (e.g., 7d) + absolute (e.g., 30d) expiry; revoke on logout; "sign out everywhere" and admin revoke delete session rows.
- CSRF: SameSite + anti-CSRF token on state-changing requests.
Token lifecycle
- Stored encrypted at rest (Data Protection); decrypted only in-memory for API calls.
- Refresh on expiry/401 via
ITokenStore→ re-encrypt + persist; failure flips account toReauthNeeded(banner + "Reconnect" CTA, see 07) — sync/AI for that account pause, the rest of the app is unaffected.
Provider OAuth specifics
| Microsoft (Graph) | ||
|---|---|---|
| Endpoint | accounts.google.com | login.microsoftonline.com (common) |
| Scopes | openid email profile gmail.readonly gmail.modify |
openid email profile offline_access Mail.Read Mail.ReadWrite |
| Identity | sub (+ verified email) |
oid/sub (+ email) |
| Refresh | refresh_token (offline) | refresh_token (offline_access) |
| Redirect | /signin/google |
/signin/microsoft |
- Least privilege: request read/modify only (no send today — matches current posture). Extra scopes are added per-feature with consent, never up-front.
Edge cases
- Same email, two providers → two distinct accounts (identity is
sub), unless the user links both to one InboxIntel user. - Provider disabled by admin flag (
provider.microsoft=false) → hide it on the picker; existing accounts of that provider pause sync and show a notice. - Reused browser / stale cookie → session validated server-side each request; revoked/expired → re-auth.