# 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.
