rebreak-monorepo/backend/docs/mail-outlook-oauth-plan.md
chahinebrini 01d515d137 feat(rebreak-native): persistent FaceID-sign-in + iOS-grouped UI + Outlook guard + sparkline cooldowns
Auth / FaceID — eingeloggt bleiben funktioniert jetzt:
- AppLock-Init idempotent: late re-init durch router.replace-Re-Mount behält
  locked-State (fixt Endlosschleife: unlock → re-mount → init reset → lock)
- LockScreen-Auto-Prompt nur wenn AppState=active (verhindert silent FaceID-
  Fail wenn LockScreen während background-Event mountet — User sah dann nur
  Fallback-Button)
- index.tsx: wenn Session schon in AsyncStorage liegt → router.replace zu /(app),
  Landing wird übersprungen; early-return nach allen Hooks (Rules of Hooks)
- WebBrowser.dismissAuthSession vor openAuthSessionAsync (verhindert
  "Another web browser is already open" nach abgebrochenen OAuth-Flows)

UI — iOS-Grouped-Look auf Settings + Profile:
- Neue Theme-Tokens groupedBg (#F2F2F7 / #000) + card (#fff / #1c1c1e),
  identisch zu Apples systemGroupedBackground / secondarySystemGroupedBackground
- settings.tsx + profile/index.tsx + profile/[userId].tsx: Page-BG → groupedBg
- StreakSection / UrgeStatsCard / DemographicsAccordion / StatsBar /
  ApprovedDomainsList: Card-BG colors.surface → colors.card

Mail-Connect — Outlook-Tile entschärft:
- Microsoft hat App-Passwords für consumer-Outlook (.com/hotmail/live/msn) im
  September 2024 abgeschaltet, der bisherige Guide-Flow ist seit ~8 Monaten
  wirkungslos → AUTHENTICATIONFAILED
- Tile bleibt sichtbar mit opacity 0.45, "Kommt bald"-Sub-Label, disabled=true
- Provider-Typ um disabled? + disabledLabelKey? erweitert (wiederverwendbar)
- Backend-OAuth-Plan unter backend/docs/mail-outlook-oauth-plan.md (mo)
  → Generisches AuthMethod-Framework (app_password | oauth) geplant

Profile — Cooldown-Verlauf als Sparkline statt Endlos-Liste:
- 8 Wochen-Buckets, Bar-Höhe nach Frequenz (cap 5/Woche), leere Wochen als
  2px-Flatlines
- Sub-Label: "{n} Cooldowns in 8 Wochen · Ø 1 pro {avg} Wochen · zuletzt {date}"
- Neutral formuliert (Sucht-/Stigma-Sensibilität: Cooldown = Schutz-Pause,
  kein Rückfall)
- useProfileData.ts liefert rawStartedAt (ISO) zusätzlich zum formatierten Wert
- i18n-Keys unter profile.cooldown.* in DE + EN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:15:54 +02:00

19 KiB

Outlook OAuth2 — Implementierungsplan

Stand: 2026-05-13
Autor: Mo (Mail-Stack-Owner)
Status: Plan, kein Code


1. Status-Recherche: Microsoft Basic-Auth-Deprecation

Was ist passiert

Microsoft hat Basic-Auth (username + password) für consumer-Outlook-Mailboxen (outlook.com, hotmail.com, hotmail.de, live.com, live.de, msn.com) schrittweise abgeschaltet:

  • September 2024: Vollständige Abschaltung für neue IMAP/POP/SMTP-Verbindungen mit Basic-Auth auf consumer-Tenants. Bestehende Verbindungen hatten eine Übergangsfrist.
  • Stand Mai 2026: Basic-Auth ist für alle consumer-Outlook-Postfächer tot. IMAP-Login mit Passwort schlägt mit [AUTHENTICATIONFAILED] fehl — egal ob App-Passwort oder normales Passwort.

Edge-Cases

Szenario Basic-Auth möglich?
outlook.com / hotmail / live / msn — consumer Nein, komplett tot
Microsoft 365 Business (firmeneigene Domain, Azure-AD-Tenant) Nein, Admins können es nicht reaktivieren
Outlook.com custom domain (eigene Domain via Outlook-Webmail) Nein, gleiche Infrastruktur
On-Premise Exchange (eigener Firmen-Server) Hypothetisch ja, aber nicht unser Use-Case

Fazit: Es gibt keinen Edge-Case der uns rettet. Der App-Passwort-Guide im ConnectMailSheet ist für Outlook-User seit September 2024 nutzlos. Jeder Outlook-User der jetzt "Verbinden" drückt bekommt vom Backend AUTHENTICATIONFAILED zurück.

Benoetigte OAuth-Scopes

Fuer IMAP read + delete via XOAUTH2 gegen Microsoft Identity Platform:

https://outlook.office.com/IMAP.AccessAsUser.All
offline_access
openid
  • IMAP.AccessAsUser.All — erlaubt IMAP-Zugriff im Namen des Users (lesen, loeschen, verschieben). Kein weiterer Mail-Scope noetig.
  • offline_access — liefert einen refresh_token (ohne ihn gibt es keinen refresh_token, nur kurze access_tokens). Pflicht fuer langlebige IDLE-Sessions.
  • openid — liefert sub/email im ID-Token fuer Account-Identifikation.

Explizit NICHT anfordern: Mail.Read, Mail.ReadWrite, Contacts.*, Calendars.*, User.Read (ausser sub/email). Minimale Scope-Anforderung.

Consumer Identity Platform vs Azure-AD

Microsoft hat zwei Systeme:

  • Microsoft Identity Platform v2 (consumers) — fuer outlook.com/hotmail-Privat- konten. Endpoint: https://login.microsoftonline.com/consumers/oauth2/v2.0/... oder tenant-agnostisch common. Azure-App-Registrierung mit "Supported account types: Personal Microsoft accounts only" oder "Any Microsoft account (multi-tenant
    • personal)".
  • Azure-AD / Entra ID (work/school) — fuer M365-Business. Nicht unser primaerer Use-Case.

Fuer Rebreak: App-Registrierung mit consumers-Endpoint — deckt alle genannten Domains ab (outlook.com, hotmail, live, msn). Wer ein M365-Business-Konto hat, faellt spaeter unter den gleichen Flow wenn wir auf common wechseln.


2. Architektur-Plan

2.1 Azure-App-Registrierung

Einmaliges Setup im Azure-Portal (portal.azure.com):

Feld Wert
Name Rebreak Mail Access
Supported account types Personal Microsoft accounts only
Redirect URI (Mobile) msauth.org.rebreak.app://auth (MSAL-Schema)
Redirect URI (Web/BFF) https://api.rebreak.org/api/mail/oauth/microsoft/callback
API Permissions IMAP.AccessAsUser.All (delegated), offline_access, openid
Client secret Ja (fuer BFF-Token-Exchange)
Public client flows Ja aktivieren (fuer PKCE)

Scopes muessen im Portal unter "API Permissions" explizit hinzugefuegt und fuer consumers-Tenant fuer alle User freigegeben werden. Kein "Grant admin consent" noetig fuer delegated permissions auf consumer-Tenant.

Multi-Tenant-Approval erforderlich? Nein. Bei "Personal Microsoft accounts only" gibt es keinen App-Review-Prozess bei Microsoft — jeder MS-User kann der App konsentieren. App-Reviews sind nur noetig wenn man All organizations-Tenant anfordert und enterprise-Features braucht.

2.2 OAuth-Flow: BFF-Pattern (Backend-mediated)

Empfehlung: BFF-Pattern, nicht PKCE direkt im Mobile-Client.

Begruendung:

  • Client-secret darf nicht im App-Bundle liegen (App-Store-Guidelines, Reverse- Engineering). PKCE ohne client_secret ist moeglich aber dann kein refresh_token via MSAL fuer native — Microsoft erlaubt es fuer public clients, aber Token- Rotation ist dann Clients-Sache.
  • Wir haben bereits den BFF-Ansatz beim Auth-Login. Konsistenz.
  • Token-Storage (encrypted, server-side) ist ohnehin Backend-Aufgabe.

Flow-Sequenz:

Native App                    Backend                      Microsoft
    |                             |                             |
    |  GET /api/mail/oauth/       |                             |
    |  microsoft/authorize        |                             |
    |  (mit state+code_challenge) |                             |
    |---------------------------->|                             |
    |                             | build auth URL              |
    |  302 redirect URL           | (PKCE, state, scopes)       |
    |<----------------------------|                             |
    |                             |                             |
    | WebBrowser.openAuthSession  |                             |
    | oeffnet MS-Login            |                             |
    |-------------------------------------------->|            |
    |                             |               | User loggt  |
    |                             |               | ein,        |
    |                             |               | konsentiert |
    |<--------------------------------------------|            |
    | redirect: .../callback?code=XXX&state=YYY   |            |
    |                             |                             |
    | POST /api/mail/oauth/       |                             |
    | microsoft/exchange          |                             |
    | body: { code, state }       |                             |
    |---------------------------->|                             |
    |                             | POST token endpoint         |
    |                             | (code + code_verifier)      |
    |                             |---------------------------->|
    |                             |<----------------------------|
    |                             | { access_token,             |
    |                             |   refresh_token,            |
    |                             |   expires_in }              |
    |                             |                             |
    |                             | decrypt+store tokens        |
    |                             | upsert MailConnection       |
    |                             |                             |
    |  { connected: true }        |                             |
    |<----------------------------|                             |

Zwei neue Backend-Endpoints:

  • GET /api/mail/oauth/microsoft/authorize — generiert state + PKCE-Verifier, speichert state temporaer in DB/Session, gibt redirect URL zurueck
  • POST /api/mail/oauth/microsoft/exchange — empfaengt code + state, tauscht gegen tokens, speichert in MailConnection

2.3 Token-Storage: Schema-Aenderung (Eskalation an rebreak-backend)

ESKALATION AN rebreak-backend erforderlich.

Das aktuelle MailConnection-Schema hat passwordEncrypted: String. Fuer OAuth brauchen wir:

// Neue Felder in MailConnection:
authMethod         String    @default("password") @map("auth_method")
// "password" | "oauth2_microsoft" | "oauth2_google" (future)

oauthAccessToken   String?   @map("oauth_access_token")   // AES-256-GCM encrypted
oauthRefreshToken  String?   @map("oauth_refresh_token")  // AES-256-GCM encrypted
oauthTokenExpiry   DateTime? @map("oauth_token_expiry")   // UTC, naechste Ablaufzeit
oauthScope         String?   @map("oauth_scope")          // gespeicherter Scope-String

passwordEncrypted bleibt fuer bestehende password-basierte Connections.

Fuer OAuth-Connections: passwordEncrypted = leer string oder "oauth" als Marker, damit bestehender Code nicht bricht. Besser: authMethod-Flag pruefe zuerst.

Schema-Migration: ALTER TABLE rebreak.mail_connections ADD COLUMN ... (4 neue Spalten). Kein Breaking Change fuer bestehende Rows.

2.4 IMAP-Connect-Logik: XOAUTH2 in ImapFlow

Gute Nachricht: imapflow (aktuell ^1.2.18) unterstuetzt XOAUTH2 nativ.

Aktueller Auth-Block in connect.post.ts und imap-idle/index.mjs:

auth: { user: email, pass: password }

Fuer OAuth: ImapFlow akzeptiert stattdessen:

auth: {
  user: email,
  accessToken: decryptedAccessToken
}

ImapFlow baut daraus automatisch den XOAUTH2-SASL-String. Kein manueller Base64-Encoding noetig, keine Library-Aenderung erforderlich.

imap-providers.ts braucht ein neues Interface:

export interface ImapAuth {
  type: 'password' | 'oauth2';
  value: string; // password (plaintext, decrypted) ODER access_token
}

Die Resolve-Logik in connect.post.ts muss authMethod aus MailConnection lesen und die richtige ImapAuth zusammenbauen.

2.5 Token-Refresh-Flow

Das haerteste Problem. Access-tokens laufen bei Microsoft nach 1 Stunde ab.

Betroffen sind zwei Stellen:

A. IMAP-Idle-Daemon (langlebige Verbindung, laeuft tage-/wochenlang):

Der Daemon muss vor jedem connect (und nach AUTHENTICATIONFAILED-Fehlern) pruefen ob der access_token noch gueltig ist. Refresh-Logik:

1. oauthTokenExpiry aus DB lesen
2. Wenn expiry < now + 5min:
   a. POST https://login.microsoftonline.com/consumers/oauth2/v2.0/token
      mit: grant_type=refresh_token, refresh_token=<decrypted>, client_id, client_secret
   b. Neues access_token + refresh_token in DB speichern (encrypted)
   c. oauthTokenExpiry updaten
3. ImapFlow mit frischem access_token verbinden

Refresh im Daemon direkt (kein HTTP-Roundtrip zum Backend noetig — Daemon hat direkten DB-Zugriff). Der Daemon erhaelt client_id + client_secret als Env-Vars.

Token-Rotation: Microsoft kann bei refresh auch ein neues refresh_token liefern ("refresh token rotation"). Daemon muss das neue refresh_token persistieren, sonst ist nach einem Refresh der naechste fehlgeschlagen.

B. scan.post.ts / connect.post.ts (kurze Connections):

Beim On-Demand-Scan: pruefe oauthTokenExpiry und refresh wenn noetig, bevor IMAP-Connection aufgebaut wird. Da dieser Code im Nitro-Kontext laeuft, kann er direkt Prisma nutzen.

Refresh-Token-Revocation bei User-Logout / Account-Loeschung: Backend muss POST https://login.microsoftonline.com/consumers/oauth2/v2.0/logout aufrufen wenn User die Verbindung trennt oder Account loescht. Sonst bleibt unsere App- Autorisierung bei Microsoft aktiv.


3. ConnectMailSheet UX-Plan (fuer rebreak-native-ui-Agent)

Geaenderter Flow fuer Outlook

Aktuell: Outlook-Provider-Tile -> Formular mit Email + App-Passwort-Hinweis + Link.

Neu: Outlook-Provider-Tile -> Anderer View (kein Passwort-Formular):

[Tile: Outlook / Hotmail / Live]
          |
          v
  View: "outlook-oauth"
  +---------------------------------+
  |  [Outlook-Icon]                 |
  |  Mit Microsoft anmelden         |
  |                                 |
  |  Rebreak benoetigt Zugriff auf  |
  |  dein Postfach um Gluecksspiel- |
  |  Mails automatisch zu loeschen. |
  |                                 |
  |  [Schild-Icon] Datenschutz:     |
  |  Wir lesen keine Inhalte. Nur   |
  |  Absender + Betreff zum Matching|
  |                                 |
  |  [Button] Mit Microsoft anmelden|
  |                                 |
  |  [Spinner waehrend OAuth laeuft]|
  +---------------------------------+

States:

  • idle: Button aktiv, Datenschutz-Hinweis sichtbar
  • loading: Button disabled, ActivityIndicator, Text "Verbindung wird hergestellt..."
  • error: Roter Error-Text unter Button (z.B. "Zugriff verweigert" wenn User Consent ablehnt, oder "Verbindung fehlgeschlagen" bei Network-Error)
  • success: Sheet schliesst sich, onSuccess() wird aufgerufen

Technisch im Client:

1. Button-Tap → GET /api/mail/oauth/microsoft/authorize
2. Backend gibt { authUrl: "https://login.microsoftonline.com/..." } zurueck
3. expo-web-browser: WebBrowser.openAuthSessionAsync(authUrl, redirectUri)
4. Deep-Link-Handler empfaengt Callback-URL mit code + state
5. POST /api/mail/oauth/microsoft/exchange mit { code, state }
6. On success: handleClose() + onSuccess()

Redirect-URI in der App: msauth.org.rebreak.app://auth — muss in app.json-Scheme registriert und in Azure-App-Registrierung eingetragen sein.

Bestehende Provider unveraendert: Gmail, iCloud, Yahoo, GMX, Other behalten den Passwort-Formular-Flow. Nur Outlook-Tile bekommt anderen View.


4. DSGVO-/Compliance-Aspekte (fuer Hans-Mueller-DSB-Review)

ESKALATION AN hans-mueller fuer formelles Review.

4.1 Microsoft als Sub-Auftragsverarbeiter

Microsoft wird durch den OAuth-Flow zusaetzlicher Sub-AV (Art. 28 DSGVO). Microsoft hat ein Standard-DPA das automatisch gilt wenn man Azure-Services nutzt (Microsoft Products and Services Data Protection Addendum — DPA). Zu pruefen:

  • Gilt das DPA auch fuer consumer Microsoft Identity Platform?
  • Muss in unserem AV-Vertraege-Verzeichnis (VVT) erwaehnt werden?
  • Microsoft hat EU-Datenzentren — Transfer-Grundlage sollte Standard-Vertragsklauseln oder Adequacy-Decision sein.

4.2 Token-Speicherung = sensibler als Passwort

Ein refresh_token gibt persistenten Zugriff auf das Postfach bis zur Revocation — laenger als ein App-Passwort (das der User jederzeit in Sekunden zurueckziehen kann). Konsequenzen:

  • Verschluesselung at-rest: gleicher AES-256-GCM wie bei passwordEncrypted. Gleicher ENCRYPTION_KEY. Kein anderer Speicherweg.
  • Zugriff auf refresh_token = Zugriff auf gesamtes Postfach. Breach-Impact hoeher als bei App-Passwort.
  • Im Datenschutzhinweis in der App und in der Datenschutzerklaerung explizit erwaehnen: "Wir speichern einen Zugriffstoken der im Namen des Users auf das Postfach zugreift".

4.3 Datenminimierung

Scopes beschraenken auf:

  • IMAP.AccessAsUser.All — Minimum fuer IMAP
  • offline_access — Minimum fuer Token-Refresh
  • openid — fuer Email-Identifikation (kein profile-Scope)

Kein User.Read.All, kein Contacts.*, kein Calendars.*.

4.4 Loeschpflicht / Widerrufs-Pflicht

Bei User-Disconnect oder Account-Loeschung:

  1. refresh_token + access_token aus DB loeschen
  2. Token bei Microsoft revoken via: POST https://login.microsoftonline.com/consumers/oauth2/v2.0/token/revoke (mit refresh_token als Parameter)

Ohne Revocation bleibt Rebreaks App-Autorisierung bei Microsoft aktiv — auch wenn wir die DB-Eintraege loeschen.

4.5 Speicherort

Token in MailConnection-Tabelle, gleicher Postgres-Host wie alle anderen User- daten. Kein separater Secret-Store noetig wenn AES-256-GCM konsistent angewandt wird.


5. Aufwands-Schaetzung

MVP-Scope

MVP = OAuth-Login funktioniert, User kann Outlook verbinden, IMAP-IDLE loescht Gambling-Mails, Token-Refresh laeuft automatisch.

Komponente Aufwand
Azure-App-Registrierung (einmaliges Setup) 0.5 Tage
Schema-Migration (4 neue Spalten, rebreak-backend) 0.5 Tage
Backend: 2 neue Endpoints (authorize + exchange) 1.5 Tage
connect.post.ts: authMethod-Logik + XOAUTH2-Support 0.5 Tage
imap-idle: Token-Refresh-Logik + XOAUTH2-Auth 1.5 Tage
scan.post.ts: Token-Refresh vor on-demand-scan 0.5 Tage
disconnect.delete.ts: Token-Revocation bei MS 0.5 Tage
ConnectMailSheet: Outlook-OAuth-View (native-ui-agent) 1.0 Tag
Deep-Link-Handling in App + expo-web-browser Setup 0.5 Tage
Testen end-to-end (inkl. Token-Refresh-Simulation) 1.0 Tag
Gesamt ~8 Personentage

Risiken

1. Microsoft Rate Limits auf Free-Tier Azure-App

Azure-Apps haben per default Rate-Limits auf den Token-Endpoint. Bei vielen Usern gleichzeitig (Token-Refresh alle ~55min pro User) koennte das ein Problem werden. Grenzwert: 30 Requests/Sekunde per App fuer /token-Endpoint. Bei 1000 aktiven Outlook-Usern: ~18 Refreshes/Minute → kein Problem. Bei 10.000 Users: Grenzwert naeherungsweise erreicht. Fruehzeitig Azure-Subscription-Limit pruefen.

2. Token-Rotation race condition im IDLE-Daemon

Wenn mehrere IDLE-Sessions parallel starten (z.B. nach Daemon-Restart) und alle gleichzeitig ein abgelaufenes Token refreshen wollen, koennen race conditions entstehen: doppelter Refresh → alter refresh_token ungueltig → zweite Session failt. Loesung: DB-Lock oder last-writer-wins mit Timestamp-Check.

3. Consumer-Tenant Consent-Screen

Beim ersten OAuth-Login sieht der User den Microsoft-Consent-Screen mit der Formulierung "Rebreak moechte auf dein Postfach zugreifen". Fuer manche User (besonders aengstliche) koennte das abschreckend wirken. Das ist kein technisches Risiko aber ein UX-Risiko — der Datenschutz-Hinweis im Sheet muss das vorab erklaeren.

4. App-Registrierung: Publisher-Verification

Microsoft kann nicht-verifizierte Publisher-Apps auf dem Consent-Screen als "unverified" markieren. Fuer Produktivbetrieb sollte Publisher-Verification in Azure abgeschlossen werden (Domain-Verifikation von rebreak.org). Aufwand: ~1 Tag einmalig. Ohne Verifikation funktioniert der Flow trotzdem, aber der Consent- Screen zeigt "unverified publisher" — schlechtes Vertrauen.

5. Apple App-Store-Review: OAuth-Flows

OAuth-Flows in iOS-Apps koennen zu App-Store-Review-Verzoegerungen fuehren wenn der Reviewer nicht einen echten Microsoft-Account zum Testen hat. Testaccount fuer Review bereitstellen (outlook.com-Testaccount mit Gambling-Mails).

6. Kein App-Review bei Microsoft selbst erforderlich

"Personal Microsoft accounts only"-Apps brauchen keine Microsoft-seitige Freigabe. Kein Warten auf MS-Approval.


6. Abhaengigkeiten und naechste Schritte

Sofortige Eskalationen

  1. rebreak-backend: Schema-Migration fuer 4 neue Felder in MailConnection. Neue Felder: auth_method, oauth_access_token, oauth_refresh_token, oauth_token_expiry. Migration kann non-destructive (additive) sein.

  2. hans-mueller: DSGVO-Review der Token-Speicherung (Abschnitt 4). Insbesondere: Microsoft als Sub-AV ins VVT aufnehmen, Datenschutzerklaerung anpassen (refresh_token = persistenter Zugriff), Revocations-Pflicht bei Loeschung.

Entscheidung vor Implementierungsstart

  • Azure-Account + App-Registrierung: wer legt an? (ops-Aufgabe)
  • client_id + client_secret: werden via Infisical verwaltet (klar), aber Infisical-Secret-Naming vorab festlegen.
  • Redirect-URI-Schema (msauth.org.rebreak.app): muss in app.json registriert sein bevor iOS-Build fuer Tests.

Kein Handlungsbedarf bis Schema-Migration done

Die Backend-Endpoints koennen erst nach dem Schema-Change implementiert werden. Warten auf rebreak-backend, dann direkt loslegen.


7. Was wir heute sofort tun koennen (ohne Schema-Change)

Unabhaengig vom OAuth-Implementierungs-Timeline:

  1. ConnectMailSheet: Outlook-Tile sofort deaktivieren oder Hinweis einblenden "Outlook wird bald unterstuetzt". Besser als den User einen Fehler erleben lassen ("AUTHENTICATIONFAILED" nach Eingabe eines App-Passworts das sowieso nicht funktioniert). Das ist eine UI-Aenderung fuer native-ui-agent.

  2. imap-providers.ts: isOAuthRequired-Flag fuer Outlook-Domains vorbereiten, damit connect.post.ts frueizeitig auf "oauth not yet implemented" antworten kann statt mit generischem Auth-Fehler zu failen.

Diese zwei Punkte koennen vor der Schema-Migration deployed werden.