From d5679f6e0782601552ffbdeb64b052700209a150 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Fri, 20 Mar 2026 09:01:35 +0100 Subject: [PATCH] hotfix msft/google login tokens end to end separated from connection --- .../OAuth-Auth-vs-Data-Connection-Konzept.md | 351 ++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 concepts/OAuth-Auth-vs-Data-Connection-Konzept.md diff --git a/concepts/OAuth-Auth-vs-Data-Connection-Konzept.md b/concepts/OAuth-Auth-vs-Data-Connection-Konzept.md new file mode 100644 index 0000000..2f06cbe --- /dev/null +++ b/concepts/OAuth-Auth-vs-Data-Connection-Konzept.md @@ -0,0 +1,351 @@ +# 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 88–94) + +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 ~166–169) — auch für den reinen Login-Pfad. + +### Microsoft — eine Scope-Liste für alles (`routeSecurityMsft.py`, Zeilen 56–67) + +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. 340–389) + +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. 387–391) + +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 450–465) + +`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 436–440. + +### Token-Refresh Microsoft (`gateway/modules/auth/tokenManager.py`, Zeilen 47–53) + +`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 150–196) + +- 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 24–31) + +`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 539–543) + +`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 436–440). 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 56–67): + +```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*