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

18 KiB
Raw Blame History

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:

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):

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:

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:

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):

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:

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):

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:

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/callbacksaveConnectionToken mit tokenPurpose=dataConnection, gleiche connectionId; UserConnection aktualisieren inkl. grantedScopes.
  • 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 dataConnectionNone 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: getConnectionTokendataConnection; 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