18 KiB
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", …)ingateway/modules/routes/routeSecurityGoogle.py. - Prefix MSFT:
APIRouter(prefix="/api/msft", …)ingateway/modules/routes/routeSecurityMsft.py. - Heute sichtbare Endpunkte:
GET /api/google/login— startet OAuth mit einem globalenSCOPES-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 globalenSCOPES(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:
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):
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)
TokenmittokenAccess = token_response["access_token"](Graph) →saveAccessToken.TokenmittokenAccess = jwt_token(Gateway-JWT) →saveAccessTokenerneut.
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)}"mittype: "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:
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ürAuthAuthority.LOCAL. - JWTs mit
authenticationAuthoritygoogle/msftwerden 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 | 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 derUserConnection) ist Pflicht.- Der Handler ruft
getCurrentUser(oder gleichwertige Session) auf, lädt die Connection übergetInterface(currentUser).getUserConnectionsbzw. verifiziert Besitz wie inrouteDataConnections.connect_service(heute Zeilen 436–440). StimmtconnectionIdnicht mitcurrentUserüberein →404/403. - Kein separater OAuth-State mit
userId/connectionIdals 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_URIals 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:falseoder 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:truenur 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 56–67):
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:
Service_GOOGLE_AUTH_CLIENT_IDService_GOOGLE_AUTH_CLIENT_SECRETService_GOOGLE_AUTH_REDIRECT_URI→ muss…/api/google/auth/login/callbackenden.Service_GOOGLE_DATA_CLIENT_IDService_GOOGLE_DATA_CLIENT_SECRETService_GOOGLE_DATA_REDIRECT_URI→ muss…/api/google/auth/connect/callbackenden.
Microsoft
Service_MSFT_AUTH_CLIENT_IDService_MSFT_AUTH_CLIENT_SECRETService_MSFT_AUTH_REDIRECT_URI→…/api/msft/auth/login/callbackService_MSFT_DATA_CLIENT_IDService_MSFT_DATA_CLIENT_SECRETService_MSFT_DATA_REDIRECT_URI→…/api/msft/auth/connect/callbackService_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
tokenPurposeNOT 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_SCOPESin die DB schreiben, wenn die Antwort etwas anderes meldet.
Soll: Verhalten pro Flow
Login
GET …/auth/login→ Redirect Google/Microsoft Auth-App, Scopes =GOOGLE_AUTH_SCOPESbzw.MSFT_AUTH_SCOPES.- Callback
…/auth/login/callback→ Code tauschen, Userinfo (oauth2/v2/userinfobzw. Graph/me), User anlegen/lesen, JWT erzeugen. - Genau ein
saveAccessToken:tokenPurpose=authSession,connectionId=null,tokenAccess=jwt,id=jtiwie heute bei Google/MSFT-JWT-Row. - Cookies setzen wie heute (
setAccessTokenCookie,setRefreshTokenCookie). - Microsoft: kein
saveAccessTokenmit Rohtoken aus Graph. - Google:
postMessageohneaccess_tokenmit Google-Bearer-Token.
Connect
- Client ruft weiter
POST /api/connections/{connectionId}/connectauf (bestehende Vertragsstelle). - Response
authUrlist relativ z. B./api/google/auth/connect?connectionId={id}bzw./api/msft/auth/connect?connectionId={id}. - Browser folgt
GET …/auth/connectmit gültiger Session → Server prüft Ownership → Redirect Data-App, Scopes =GOOGLE_DATA_SCOPES/MSFT_DATA_SCOPES. - Callback
…/auth/connect/callback→saveConnectionTokenmittokenPurpose=dataConnection, gleicheconnectionId;UserConnectionaktualisieren inkl.grantedScopes.
Admin Consent (MSFT)
GET /api/msft/adminconsentverwendetService_MSFT_DATA_CLIENT_IDundredirect_uriabgeleitet vonService_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)
- Vier OAuth-Apps in Google Cloud / Zwei in Azure (Auth + Data) anlegen; Redirect-URIs exakt wie oben.
- DB-Migration
tokenPurposeNOT NULL; Datenbereinigung / erzwungener Re-Login und Re-Connect. - Konstanten und Handler in
routeSecurityGoogle.py/routeSecurityMsft.pyimplementieren; alte Routen entfernen. routeDataConnections.pyauthUrlanpassen.interfaceDbApp+authentication.py+tokenManager+mainServiceSecurity.pyanpassen.csrf.py+ alleenv_*.envaktualisieren.- Frontend-URLs ersetzen.
- Tests: Login-Tokeninfo ohne Gmail/Drive; Connect mit Daten-Scopes; Refresh MSFT mit vollständigem Scope-String; API-Aufruf mit altem
/api/*/loginmuss 404 liefern.
Tests (Akzeptanzkriterien)
GET /api/google/loginundGET /api/msft/loginexistieren nicht (404).- Nach Login liefert Google
tokeninfo(falls mit kurzlebigenm Token geprüft) keinegmail/driveScope-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 überConnectorResolverfunktioniert. refreshMicrosoftTokensendetscope= vollständigerMSFT_DATA_SCOPES-String.postMessagebeim Google-Login enthält keinen Googleaccess_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