Files
Inboxintel/docs/discovery/multi-provider/02-auth-and-signin.md
T
cesnimda 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
docs: discovery blueprint + multi-provider design (#8)
2026-07-01 19:59:53 +02:00

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 )
  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) — 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.