# 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 ) ``` 1. User picks a provider → redirect to provider OAuth (**PKCE**, `state`, `nonce`). 2. Callback → exchange code → `GetIdentityAsync` returns `(provider, sub, email, name)`. 3. **Resolve:** look up `accounts (provider, sub)`. - **No match →** first-time. Create `user` (**first user ever = Admin**, otherwise `Member` if `system_settings.registration_open`, else reject) + `account` (`is_login_identity=true`) + encrypted `provider_tokens`. Start session. - **Match →** existing user. Refresh tokens, start session. 4. 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. - 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** (`sessions` table) 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 to **`ReauthNeeded`** (banner + "Reconnect" CTA, see [07](07-ux-flows.md)) — sync/AI for that account pause, the rest of the app is unaffected. ## Provider OAuth specifics | | Google | 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.