Social login

auth.oauth — Login via external providers (OIDC & plain OAuth2)

Sign in through an external provider; opens a normal Qino session via login(). Two kinds:

  • OIDC (Google, Microsoft, GitLab, Auth0, Slack, Keycloak, …) — endpoints via discovery, identity from the id_token, Authorization Code + PKCE.
  • plain OAuth2 (GitHub, Discord, …) — explicit endpoints, identity from a userinfo call with the access token (no id_token).

Per provider: authorize_url set → OAuth2; empty and issuer set → OIDC discovery.

Konfigurieren

Im Backend-Modul cms.backend.superuser.security.auth.oauth (Superuser → Security → Authentication → Login providers). Gängige Provider sind als Vorlagen angelegt — meist nur client_id/client_secret eintragen. Die Redirect-URI für den Provider zeigt das Formular an: {appUrl}oauth/callback/<name>.

Tabelle oauth_provider:

Feld Zweck
name Slug in der URL (url-safe/lowercase)
issuer OIDC: Basis-URL für Discovery, z.B. https://accounts.google.com
authorize_url / token_url / userinfo_url OAuth2: explizite Endpunkte (kein Discovery)
email_url OAuth2 optional: separater E-Mail-Endpoint (z.B. GitHub /user/emails)
client_id / client_secret vom Provider
scopes leer = openid email profile
auto_create 1 = unbekannte, verifizierte User anlegen, 0 = nur bestehende
allowed_domains optional, kommagetrennt (z.B. example.com) — begrenzt öffentliche Provider
trust_email 1 = E-Mails ohne Bestätigungs-Flag gelten für neue Konten als bestätigt (Facebook liefert keins); bestehende Konten nur über Verknüpfung
redirect_uri optional, geteilter Callback statt {appUrl}oauth/callback/<name> (siehe auth.oauth.relay)
response_mode optional, z.B. form_post (Apple verlangt es für name/email)
key_id / team_id gesetzt = client_secret ist ein privater ES256-Schlüssel; das Modul signiert daraus pro Login ein JWT (Apple)

Viele Domains, ein Eintrag beim Provider

Mit redirect_uri zeigt der Provider auf einen geteilten Callback, z.B. auth.oauth.relay — dann muss beim Provider nicht jede Domain eingetragen werden. Die eigene Callback-URL reist dabei im state mit.

Wer mit wem verknüpft ist

Tabelle oauth_provider_usr (provider, sub) → usr_id speichert beim ersten Login die stabile Id des Providers (OIDC sub, sonst id). Danach zählt diese Verknüpfung, nicht mehr die E-Mail — ändert sich die E-Mail auf einer Seite, bleibt das Konto dasselbe. Ohne Verknüpfung wird per verifizierter E-Mail zugeordnet (oder angelegt) und die Verknüpfung gespeichert.

allowed_domains gilt, wo eine E-Mail mitkommt; eine bestehende Verknüpfung bleibt aber, auch wenn der Provider später keine E-Mail mehr schickt (Apple). Die Backend-Seite listet alle Verknüpfungen und kann sie einzeln lösen; danach gilt wieder die E-Mail. Wird ein Provider gelöscht, verschwinden seine Verknüpfungen.

Mitgelieferte Presets

  • OIDC: google, microsoft, apple, auth0, gitlab, linkedin, slack (<tenant> bei microsoft/auth0 durch deine Domain/Tenant ersetzen).
  • OAuth2: github, discord.
  • apple: client_id = Services ID, client_secret = Inhalt der .p8-Datei, dazu key_id und team_id — das kurzlebige JWT erzeugt das Modul selbst. Antwortet per form_post; der Callback macht daraus einen GET, damit das Session-Cookie (SameSite=Lax) mitkommt.
  • microsoft mit common: iss nennt den Tenant des Users, geprüft gegen {tenantid} aus der Discovery. Microsoft schickt kein email_verified — in der App-Registrierung das optionale Claim xms_edov aktivieren, sonst gelten E-Mails als unbestätigt.

Weitere OAuth2-Provider: neue Zeile mit authorize_url, token_url, userinfo_url (+ ggf. email_url).

Login starten

cms.cont.login.oauth zeigt einen „Log in with …"-Link pro Provider, cms.cont.my.oauth zeigt dem Benutzer seine Verknüpfungen und verbindet weitere. Oder direkt verlinken:

html
<a href="{appUrl}oauth/start/github">Login mit GitHub</a>

Optional ?return_to=/pfad (nur lokale Pfade werden akzeptiert).

Sicherheit (was durchgesetzt wird)

  • OIDC: Authorization Code + PKCE (S256); state, nonce, code_verifier liegen in der Session und werden im Callback einmal geprüft und verbraucht.
  • id_token: iss, aud, nonce, exp werden geprüft. Die Signatur bewusst nicht (jwks_uri) — im Code-Flow kommt das Token per TLS direkt vom token_endpoint (OIDC erlaubt das).
  • OAuth2: kein id_token/nonce; state schützt vor CSRF, die Identität kommt vom userinfo-Call.
  • email_verified: ein explizit unverifiziertes E-Mail-Claim wird abgelehnt.
  • Open-Redirect: return_to nur lokale Pfade (kein //, kein Schema).
  • Backend: client_secret wird nie zurückgerendert.
  • Achtung Account-Linking: beim ersten Login landet eine verifizierte E-Mail im bestehenden Qino-User mit derselben E-Mail (auch einem Superuser); danach zählt die Verknüpfung. Mit auto_create bei einem öffentlichen Provider kommt jeder mit Konto dort rein (ohne Rechte) — dann allowed_domains setzen.

Offen / verbesserungswürdig

Kern: - JWKS-Signaturprüfung als Defense-in-Depth; azp prüfen bei mehreren aud. - Refresh-Tokens / RP-Logout (end_session_endpoint): nicht implementiert. - Zwei Logins parallel (zwei Tabs) überschreiben sich in der Session → Fix: per state speichern.

Backend (bewusst schlank): - Keine name-Validierung; doppelter Name → DB-Fehler (500). - Kein Speichern/Löschen-Feedback, kein „Vorlage vs. konfiguriert"-Badge.

Sonstiges: - Automatisch getestet sind resolveUser()/identity(); der Callback (Token-Tausch, id_token-Prüfung) nur manuell.