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