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

477 lines
19 KiB
Markdown

# 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:
```prisma
// 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`:
```js
auth: { user: email, pass: password }
```
Fuer OAuth: ImapFlow akzeptiert stattdessen:
```js
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:
```ts
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.