wiki/concepts/OAuth-Auth-vs-Data-Connection-Konzept.md

351 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# OAuth: Login (Auth-only) vs. UserConnection (Data) — Zielzustand & Umsetzungsplan
## Übersicht
Dieses Dokument beschreibt den **ausschließlichen Zielzustand** der Google- und Microsoft-Integration im **PowerOn Gateway** (`poweron/gateway`). Es gibt **keine Übergangsphase** und **keine Rückwärtskompatibilität**.
Der folgende Abschnitt **„Ist (Codebasis)“** belegt den Ausgangspunkt mit konkreten Stellen im Repo. Alles danach ist **Soll**: feste URLs, feste Scope-Listen, feste Env-Keys, feste Verhaltensregeln.
| Ebene | OAuth-Client | Token in DB |
|--------|----------------|-------------|
| **Identity (Login)** | Auth-App | `connectionId = null`, `tokenPurpose = authSession`, `tokenAccess` = Gateway-JWT |
| **Data (UserConnection)** | Data-App | `connectionId` gesetzt, `tokenPurpose = dataConnection`, `tokenAccess` / `tokenRefresh` = Microsoft/Google |
---
## Ist (Codebasis) — Referenz für den Umbau
### Router & URLs (heute)
- **Prefix Google:** `APIRouter(prefix="/api/google", …)` in `gateway/modules/routes/routeSecurityGoogle.py`.
- **Prefix MSFT:** `APIRouter(prefix="/api/msft", …)` in `gateway/modules/routes/routeSecurityMsft.py`.
- **Heute sichtbare Endpunkte:**
- `GET /api/google/login` — startet OAuth mit **einem** globalen `SCOPES`-Array (enthält Gmail + Drive + Userinfo + `openid`).
- `GET /api/google/auth/callback` — gemeinsamer Callback für Login (`state.type == "login"`) und Connect (`state.type == "connect"`).
- `GET /api/msft/login` — startet OAuth mit **einem** globalen `SCOPES` (alle Business-Scopes).
- `GET /api/msft/auth/callback` — gemeinsamer Callback Login/Connect.
- `GET /api/msft/adminconsent` + `GET /api/msft/adminconsent/callback`.
- `GET /api/google/config` — Debug-Konfiguration.
### Google — eine Scope-Liste für alles (`routeSecurityGoogle.py`, ca. Zeilen 8894)
Heute:
```text
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/userinfo.email
openid
```
Zusätzlich setzt der Login `access_type=offline`, `include_granted_scopes=true` (Zeilen ~166169) — auch für den reinen Login-Pfad.
### Microsoft — eine Scope-Liste für alles (`routeSecurityMsft.py`, Zeilen 5667)
Heute (delegiert, an MSAL übergeben; MSAL ergänzt u. a. `openid`, `profile`, `offline_access`):
```text
User.Read
Mail.ReadWrite
Mail.Send
Files.ReadWrite.All
Sites.ReadWrite.All
Team.ReadBasic.All
OnlineMeetings.Read
Chat.ReadWrite
ChatMessage.Send
```
Kommentar im Code: Admin Consent unter `GET /api/msft/adminconsent`.
### Microsoft Login speichert zwei Access-Token-Rows (`routeSecurityMsft.py`, ca. 340389)
1. `Token` mit `tokenAccess = token_response["access_token"]` (Graph) → `saveAccessToken`.
2. `Token` mit `tokenAccess = jwt_token` (Gateway-JWT) → `saveAccessToken` erneut.
Der Login-Pfad soll im Ziel **nur** Schritt 2 persistieren (ein Row, `authSession`).
### Google Login-Callback postet Provider-Token ans Frontend (`routeSecurityGoogle.py`, ca. 387391)
Im HTML wird `postMessage` mit `access_token: token_response["access_token"]` (Google Access Token) mitgeschickt. **Ziel:** Im Login-Flow kein Google Access Token an `window.opener` senden — nur noch `token_data` für das Gateway-JWT (oder nur Cookies, je nach UI-Vertrag).
### Connections → OAuth-Start (`routeDataConnections.py`, Zeilen 450465)
`connect_service` baut:
- MSFT: `auth_url = f"/api/msft/login?state={json.dumps(state_data)}"` mit `type: "connect"`, `connectionId`, `userId`.
- Google: `auth_url = f"/api/google/login?state=…"` gleiches Muster.
**Ziel:** Diese Strings werden durch die neuen **Connect-URLs** ersetzt (siehe unten). Die fachliche Validierung „Connection gehört `currentUser`“ bleibt: Zeilen 436440.
### Token-Refresh Microsoft (`gateway/modules/auth/tokenManager.py`, Zeilen 4753)
`refreshMicrosoftToken` sendet beim Refresh einen **fest eingeschränkten** `scope`:
```text
Mail.ReadWrite Mail.Send Mail.ReadWrite.Shared User.Read
```
Das deckt **nicht** die in `routeSecurityMsft.py` verwendeten Scopes ab (`Files.ReadWrite.All`, `Sites.ReadWrite.All`, Teams, …). **Ziel:** Refresh-`scope` ist **zeichenidentisch** zur Data-Scope-Liste aus dem Connect (eine Konstante `MSFT_DATA_SCOPES` im Code, für Authorize + Token + Refresh).
### JWT-Validierung (`gateway/modules/auth/authentication.py`, Zeilen 150196)
- Datenbankprüfung (`findActiveTokenById`) gilt **nur** für `AuthAuthority.LOCAL`.
- JWTs mit `authenticationAuthority` `google` / `msft` werden **ohne** diese DB-Zeilenprüfung akzeptiert, sofern Signatur und Claims passen.
**Ziel:** Für `google` und `msft` dieselbe Regel wie LOCAL: aktive Row mit `id = jti`, `tokenPurpose = authSession`, `authority` passend. (Kein „optional“.)
### CSRF (`gateway/modules/auth/csrf.py`, Zeilen 2431)
`exempt_paths` enthält u. a.:
- `/api/msft/login`, `/api/google/login`
- `/api/msft/callback`, `/api/google/callback` — diese Pfade entsprechen **nicht** den realen Callback-Routen (`/api/msft/auth/callback`, `/api/google/auth/callback`).
**Ziel:** `exempt_paths` auf die **neuen** OAuth-GET-Start-URLs setzen (siehe unten). Tote Einträge entfernen. (Hinweis: CSRF greift nur für `POST`/`PUT`/`DELETE`/`PATCH`; die Einträge sind trotzdem mit den echten Pfaden zu synchronisieren.)
### Konfiguration (`gateway/env_dev.env`, `env_int.env`, `env_prod.env`, `.env`)
Heute u. a.:
- `Service_GOOGLE_CLIENT_ID`, `Service_GOOGLE_CLIENT_SECRET`, `Service_GOOGLE_REDIRECT_URI` → zeigen auf **eine** App und Callback `…/api/google/auth/callback`.
- `Service_MSFT_CLIENT_ID`, `Service_MSFT_CLIENT_SECRET`, `Service_MSFT_REDIRECT_URI``…/api/msft/auth/callback`.
**Ziel:** Diese Keys werden **ersetzt** (siehe Abschnitt Konfiguration).
### App-Einbindung (`gateway/app.py`, ca. Zeilen 539543)
`msftRouter` und `googleRouter` werden eingebunden; der Umbau bleibt in denselben Modulen oder splittet intern — **keine** parallelen Legacy-Router.
---
## Soll: Öffentliche HTTP-API (nur diese Routen)
Die heutigen Routen **`GET /api/google/login`**, **`GET /api/msft/login`** und der **gemeinsame** Callback **`GET /api/google/auth/callback`** / **`GET /api/msft/auth/callback`** werden **entfernt**. Stattdessen:
| Aktion | Google | Microsoft |
|--------|--------|-----------|
| Login start | `GET /api/google/auth/login` | `GET /api/msft/auth/login` |
| Login callback | `GET /api/google/auth/login/callback` | `GET /api/msft/auth/login/callback` |
| Connect start | `GET /api/google/auth/connect` | `GET /api/msft/auth/connect` |
| Connect callback | `GET /api/google/auth/connect/callback` | `GET /api/msft/auth/connect/callback` |
| Admin Consent | — | `GET /api/msft/adminconsent` |
| Admin Consent Callback | — | `GET /api/msft/adminconsent/callback` |
**Connect-Start:** Query-Parameter und Pflichten — **konkret:**
- `connectionId` (UUID der `UserConnection`) ist **Pflicht**.
- Der Handler ruft `getCurrentUser` (oder gleichwertige Session) auf, lädt die Connection über `getInterface(currentUser).getUserConnections` bzw. verifiziert Besitz wie in `routeDataConnections.connect_service` (heute Zeilen 436440). Stimmt `connectionId` nicht mit `currentUser` überein → `404` / `403`.
- **Kein** separater OAuth-State mit `userId`/`connectionId` als alleinige Sicherheit: Session + serverseitige Ownership-Prüfung sind maßgeblich.
**Redirects in Google Cloud / Azure (pro Umgebung):**
- Auth-App: genau die beiden URIs `…/auth/login/callback`.
- Data-App: genau die beiden URIs `…/auth/connect/callback`.
- MSFT Admin Consent Redirect: wie heute aus dem Data-Redirect URI abgeleitet, aber mit dem **Data-**`REDIRECT_URI` als Basis:
Heute: `REDIRECT_URI.replace("/auth/callback", "/adminconsent/callback")`.
**Ziel:** `Service_MSFT_DATA_REDIRECT_URI.replace("/auth/connect/callback", "/adminconsent/callback")` (oder gleiche relative Ersetzung auf dem neuen Connect-Callback-Pfad).
Debug **`GET /api/google/config`:** entweder entfernen oder auf **Auth-** und **Data-**Client getrennt dokumentieren (ohne Secrets); nicht zwingend für Produktion.
---
## Soll: Scope-Listen (fest im Code)
### Google — Auth (`GOOGLE_AUTH_SCOPES`)
Nur OIDC + Profil, **keine** Gmail-/Drive-API-Scopes:
```text
openid
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
```
- **`access_type`:** `online` (kein Refresh-Token-Zwang für Login).
- **`include_granted_scopes`:** **`false`** oder Parameter weglassen.
### Google — Data (`GOOGLE_DATA_SCOPES`)
Übernahme der **heutigen** Daten-Scopes aus `routeSecurityGoogle.py`, ohne die Userinfo-Scopes zu duplizieren, wenn sie für den Data-Flow nicht nötig sind; **Minimum** zur Kompatibilität mit `connectorGoogle.py` (Drive + Gmail):
```text
openid
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/drive.readonly
```
- **`access_type`:** `offline`.
- **`include_granted_scopes`:** `true` **nur** hier (Data-App / Connect), nicht im Auth-Login.
### Microsoft — Auth (`MSFT_AUTH_SCOPES`)
Nur Profil für `GET https://graph.microsoft.com/v1.0/me` nach Code-Tausch:
```text
User.Read
```
MSAL erhält **ausschließlich** diese Liste für `get_authorization_request_url` / `acquire_token_by_authorization_code` im **Login**-Pfad. Die von MSAL ergänzten Standard-Claims (`openid`, `profile`, `offline_access`, …) bleiben unverändert Provider-seitig.
### Microsoft — Data (`MSFT_DATA_SCOPES`)
**Unverändert** gegenüber der heutigen Liste in `routeSecurityMsft.py` (Zeilen 5667):
```text
User.Read
Mail.ReadWrite
Mail.Send
Files.ReadWrite.All
Sites.ReadWrite.All
Team.ReadBasic.All
OnlineMeetings.Read
Chat.ReadWrite
ChatMessage.Send
```
**Regel:** `MSFT_DATA_SCOPES` ist **eine** Konstante; sie wird für Authorize, für `acquire_token_by_authorization_code` Connect und für den **Refresh** (`tokenManager.refreshMicrosoftToken`, Feld `scope` im POST an `…/oauth2/v2.0/token`) als **eine** Space-getrennte Zeichenkette verwendet (Reihenfolge wie oben, konsistent halten).
---
## Soll: Konfiguration (`gateway/env_*.env` / `APP_CONFIG`)
Die bisherigen Einträge werden **entfernt** und **ersetzt** durch:
**Google**
- `Service_GOOGLE_AUTH_CLIENT_ID`
- `Service_GOOGLE_AUTH_CLIENT_SECRET`
- `Service_GOOGLE_AUTH_REDIRECT_URI` → muss `…/api/google/auth/login/callback` enden.
- `Service_GOOGLE_DATA_CLIENT_ID`
- `Service_GOOGLE_DATA_CLIENT_SECRET`
- `Service_GOOGLE_DATA_REDIRECT_URI` → muss `…/api/google/auth/connect/callback` enden.
**Microsoft**
- `Service_MSFT_AUTH_CLIENT_ID`
- `Service_MSFT_AUTH_CLIENT_SECRET`
- `Service_MSFT_AUTH_REDIRECT_URI``…/api/msft/auth/login/callback`
- `Service_MSFT_DATA_CLIENT_ID`
- `Service_MSFT_DATA_CLIENT_SECRET`
- `Service_MSFT_DATA_REDIRECT_URI``…/api/msft/auth/connect/callback`
- `Service_MSFT_TENANT_ID` (unverändert, z. B. `common`)
**Zu aktualisierende Dateien:** `gateway/env_dev.env`, `gateway/env_int.env`, `gateway/env_prod.env`, `gateway/.env`, sowie jede zentrale Konfiguration, die `APP_CONFIG` befüllt.
**`TokenManager`** lädt für Refresh **ausschließlich** Data-Client-Zugangsdaten: `Service_MSFT_DATA_CLIENT_ID`, `Service_MSFT_DATA_CLIENT_SECRET`, `Service_MSFT_TENANT_ID` (heute nutzt die Klasse `Service_MSFT_CLIENT_ID` / `SECRET` — das wird ersetzt).
---
## Soll: Datenmodell & DB
### `Token` (`gateway/modules/datamodels/datamodelSecurity.py`)
Pflichtfeld:
```text
tokenPurpose: Literal["authSession", "dataConnection"]
```
| `tokenPurpose` | `connectionId` | `tokenAccess` | `tokenRefresh` |
|----------------|----------------|---------------|----------------|
| `authSession` | `null` | Gateway-JWT | leer oder nicht genutzt |
| `dataConnection` | gesetzt | Provider Access | Provider Refresh (Data-Flow) |
Kein weiteres optionales Metadatenfeld im ersten Schritt.
### Migration
- Spalte `tokenPurpose` **NOT NULL** mit Check-Constraint oder App-Validierung.
- Cutover: bestehende Datenbankzeilen ohne klare Zuordnung werden **bereinigt**; Nutzer führen Login und Connect **einmal neu** aus.
### `UserConnection.grantedScopes`
Nach Connect-Callback:
- **Google:** aus `token_response["scope"]` (bzw. aus der Antwort des Token-Endpunkts) als Liste splitten.
- **Microsoft:** aus dem erfolgreichen MSAL-Resultat-Feld für Scope, falls vorhanden; wenn der Provider nur eine Teilmenge zurückgibt, persistieren was zurückkommt — **nicht** blind `MSFT_DATA_SCOPES` in die DB schreiben, wenn die Antwort etwas anderes meldet.
---
## Soll: Verhalten pro Flow
### Login
1. `GET …/auth/login` → Redirect Google/Microsoft **Auth-App**, Scopes = `GOOGLE_AUTH_SCOPES` bzw. `MSFT_AUTH_SCOPES`.
2. Callback `…/auth/login/callback` → Code tauschen, Userinfo (`oauth2/v2/userinfo` bzw. Graph `/me`), User anlegen/lesen, JWT erzeugen.
3. Genau **ein** `saveAccessToken`: `tokenPurpose=authSession`, `connectionId=null`, `tokenAccess=jwt`, `id=jti` wie heute bei Google/MSFT-JWT-Row.
4. Cookies setzen wie heute (`setAccessTokenCookie`, `setRefreshTokenCookie`).
5. **Microsoft:** kein `saveAccessToken` mit Rohtoken aus Graph.
6. **Google:** `postMessage` **ohne** `access_token` mit Google-Bearer-Token.
### Connect
1. Client ruft weiter `POST /api/connections/{connectionId}/connect` auf (bestehende Vertragsstelle).
2. Response `authUrl` ist **relativ** z. B. `/api/google/auth/connect?connectionId={id}` bzw. `/api/msft/auth/connect?connectionId={id}`.
3. Browser folgt `GET …/auth/connect` mit gültiger Session → Server prüft Ownership → Redirect **Data-App**, Scopes = `GOOGLE_DATA_SCOPES` / `MSFT_DATA_SCOPES`.
4. Callback `…/auth/connect/callback``saveConnectionToken` mit `tokenPurpose=dataConnection`, gleiche `connectionId`; `UserConnection` aktualisieren inkl. `grantedScopes`.
### Admin Consent (MSFT)
- `GET /api/msft/adminconsent` verwendet **`Service_MSFT_DATA_CLIENT_ID`** und `redirect_uri` abgeleitet von **`Service_MSFT_DATA_REDIRECT_URI`**.
---
## Soll: Codeänderungen (Dateien)
| Datei | Konkrete Aufgabe |
|-------|------------------|
| `gateway/modules/routes/routeSecurityGoogle.py` | Router neu strukturieren: vier Zielrouten; zwei Client-Paare aus Config; Konstanten `GOOGLE_AUTH_SCOPES` / `GOOGLE_DATA_SCOPES`; Login ohne `include_granted_scopes`; Connect mit `offline` + `include_granted_scopes`; `/login` und `/auth/callback` löschen. |
| `gateway/modules/routes/routeSecurityMsft.py` | Wie oben; `MSFT_AUTH_SCOPES` / `MSFT_DATA_SCOPES`; Login nur eine `saveAccessToken`-Row; Connect unverändert fachlich, aber nur Data-Client; Admin Consent auf Data-Client; `/login` und `/auth/callback` löschen. |
| `gateway/modules/routes/routeDataConnections.py` | `auth_url` auf `/api/msft/auth/connect?connectionId=…` und `/api/google/auth/connect?connectionId=…` (kein JSON-`state` mehr nötig für Security — Session + DB-Check). |
| `gateway/modules/interfaces/interfaceDbApp.py` | `saveAccessToken` / `saveConnectionToken` validieren `tokenPurpose` und `connectionId` wie in der Tabelle oben; Verstoß → `ValueError`. |
| `gateway/modules/auth/tokenManager.py` | `msft_client_id` / `secret` aus `Service_MSFT_DATA_*`; Refresh-`scope` = space-joined `MSFT_DATA_SCOPES`; Refresh nur sinnvoll aufrufen wenn `oldToken.tokenPurpose == dataConnection` (oder implizit nur solche Tokens in DB). |
| `gateway/modules/serviceCenter/core/serviceSecurity/mainServiceSecurity.py` | `getFreshToken`: wenn geladener Token nicht `dataConnection``None` zurück + Log. |
| `gateway/modules/auth/authentication.py` | Erweiterung: für `authenticationAuthority` `google` und `msft` dieselbe aktive-Row-Prüfung wie LOCAL auf `findActiveTokenById` mit passender `authority` und `tokenPurpose=authSession`. |
| `gateway/modules/auth/csrf.py` | `exempt_paths`: `/api/google/auth/login`, `/api/msft/auth/login` (und keine toten `/api/*/callback` ohne `/auth/`). |
| `gateway/env_*.env`, `.env` | Keys wie im Abschnitt Konfiguration. |
| Frontend (PowerOn UI) | Alle Links von `/api/google/login``/api/google/auth/login`, von `/api/msft/login``/api/msft/auth/login`; `connect` nutzt neue `authUrl`. |
---
## Umsetzungsschritte (ein Schnitt, Reihenfolge)
1. Vier OAuth-Apps in Google Cloud / Zwei in Azure (Auth + Data) anlegen; Redirect-URIs exakt wie oben.
2. DB-Migration `tokenPurpose` NOT NULL; Datenbereinigung / erzwungener Re-Login und Re-Connect.
3. Konstanten und Handler in `routeSecurityGoogle.py` / `routeSecurityMsft.py` implementieren; alte Routen entfernen.
4. `routeDataConnections.py` `authUrl` anpassen.
5. `interfaceDbApp` + `authentication.py` + `tokenManager` + `mainServiceSecurity.py` anpassen.
6. `csrf.py` + alle `env_*.env` aktualisieren.
7. Frontend-URLs ersetzen.
8. Tests: Login-Tokeninfo ohne Gmail/Drive; Connect mit Daten-Scopes; Refresh MSFT mit vollständigem Scope-String; API-Aufruf mit altem `/api/*/login` muss **404** liefern.
---
## Tests (Akzeptanzkriterien)
- [ ] `GET /api/google/login` und `GET /api/msft/login` existieren **nicht** (404).
- [ ] Nach Login liefert Google `tokeninfo` (falls mit kurzlebigenm Token geprüft) **keine** `gmail`/`drive` Scope-Strings.
- [ ] Nach MSFT-Login enthält das Access Token **keine** Ressourcen-Zustimmung für Mail/Files jenseits von `User.Read` (Scope-String prüfen).
- [ ] Nach Connect: `getConnectionToken``dataConnection`; Browse über `ConnectorResolver` funktioniert.
- [ ] `refreshMicrosoftToken` sendet `scope` = vollständiger `MSFT_DATA_SCOPES`-String.
- [ ] `postMessage` beim Google-Login enthält **keinen** Google `access_token`.
---
## Changelog (Dokument)
| Datum | Änderung |
|-------|----------|
| 2026-03-20 | Erstversion |
| 2026-03-20 | Nur Zielzustand, ohne Übergang |
| 2026-03-20 | Ist-Analyse am Code; fest definierte Scopes, URLs, Env-Keys; vage Formulierungen entfernt |
---
*Ende Dokument*