User-Mapping Nextcloud Musikportal
Detlef • 2025-08-10 11:02:51
Hallo ChatGPT! An meiner Nextcloud hängt unter anderem ein externes Musikportal von mir, welches ich auch von der Nextcloud aus auch aufrufen kann. Allerdings müssen sich alle Benutzer, die in der Nextcloud bereits angemeldet sind, in dem Musikportal noch einmal anmelden. Das ist sehr umständlich. Wie kann hier ein User-Mapping aussehen?
ChatGPT-4.0 • 2025-08-10 11:02:52
Okay, ich verstehe dein Problem:
Du hast eine **Nextcloud-Instanz** und daneben ein **externes Musikportal**. Beide haben ihre eigene Benutzerverwaltung, und aktuell muss man sich **zweimal** anmelden – einmal in Nextcloud und einmal im Musikportal.
Das Ziel ist also **Single Sign-On** (SSO) oder zumindest **User-Mapping**, damit die Anmeldung in Nextcloud automatisch auch fürs Musikportal gilt.
---
## 1️⃣ Grundidee: Single Sign-On statt getrennte Logins
Anstatt in beiden Systemen eine eigene Session zu haben, kannst du die **Nextcloud-Auth** als gemeinsame Basis nehmen.
Das geht mit zwei Hauptwegen:
| Methode | Vorteile | Nachteile |
|--------------------------|----------|-----------|
| **SSO via OAuth2 / OpenID Connect** (OIDC) | Standardisiert, sicher, skaliert auf viele Apps | Musikportal muss OIDC unterstützen oder Middleware nötig |
| **User-Mapping über LDAP/AD** | Ein zentrales Benutzerverzeichnis | Admin-Aufwand für LDAP-Setup, evtl. Overkill für kleines Setup |
| **Reverse Proxy mit SSO** (z. B. Authelia, Keycloak Gatekeeper) | Einheitlicher Login für alle Webapps | Mehr Serverkomponenten nötig |
---
## 2️⃣ User-Mapping-Ansatz
User-Mapping bedeutet:
- Beide Systeme kennen denselben Benutzer **unter derselben eindeutigen ID** (z. B. E-Mail oder Benutzername)
- Die Authentifizierung kommt nur aus einer Quelle (z. B. Nextcloud)
### Beispiel: Nextcloud als OAuth2 Provider
1. **Nextcloud-App aktivieren**:
- Gehe in Nextcloud zu **Apps → Integration**
- Installiere **OAuth 2.0** oder **OpenID Connect** App (in neueren Versionen „OpenID Connect Provider“ genannt)
2. **Musikportal als Client registrieren**:
- In Nextcloud unter **Einstellungen → Sicherheit → OAuth 2.0**
- Neues Client-Secret und Redirect-URL vom Musikportal hinterlegen
3. **Musikportal OIDC-fähig machen**:
- Falls es OIDC/OAuth2 kann: einfach Client-ID & Secret hinterlegen
- Falls nicht: Middleware wie **Keycloak**, **Dex** oder **Authelia** dazwischen schalten
4. **User-Mapping**:
- Als *Subjekt-Identifikator* in OIDC das Nextcloud-Feld `uid` oder `email` verwenden
- Musikportal muss dieses Feld als eindeutige User-ID speichern, sodass beim Login sofort der passende Account geladen wird
---
## 3️⃣ Alternative: Gemeinsame Benutzerquelle (LDAP)
Falls dein Musikportal **LDAP** versteht:
- In Nextcloud **LDAP/AD-Integration** aktivieren
- Musikportal ebenfalls an denselben LDAP binden
- Damit nutzen beide Systeme **denselben Login und dasselbe Passwort** (kein SSO im engeren Sinne, aber identische Credentials)
---
## 4️⃣ Reverse Proxy Variante (schneller Hack)
Wenn du **keine tiefen Änderungen** am Musikportal machen kannst:
- Nutze einen Reverse Proxy wie **Traefik + ForwardAuth**, **Caddy + Authelia** oder **NGINX mit OAuth2-Proxy**
- Der Proxy prüft, ob der Benutzer in Nextcloud eingeloggt ist (z. B. über ein Session-Cookie oder SSO-Dienst)
- Ist er nicht eingeloggt, wird er zur Nextcloud-Loginseite umgeleitet
- Vorteil: Funktioniert oft ohne Änderung am Zielsystem
- Nachteil: User-Mapping muss über die E-Mail/Benutzer-ID im Token erfolgen
---
💡 **Mein Tipp für dich:**
Wenn dein Musikportal **OIDC kann**, ist der sauberste Weg, die Nextcloud-App „OpenID Connect Provider“ zu aktivieren und Nextcloud als **IdP** (Identity Provider) zu nutzen. Dann gibt’s nur noch *einen* Login, und alle Nextcloud-User sind automatisch im Musikportal angemeldet – ohne dass du separate Passwörter verwalten musst.
---
Falls du magst, könnte ich dir als Nächstes einen **konkreten Beispiel-Flow mit Nextcloud + OIDC** skizzieren, inkl. Token-Struktur und User-Mapping-Feldern.
Das wäre die schnellste und zukunftssicherste Lösung.
Detlef • 2025-08-10 11:08:27
Das Musikportal ist eine von mir sehr stark erweitertes pico_cms.
Hier einmal die LocalAuth.yml:
---
users:
janine_sanders:
pwhash: $2y$15$sT6ctWBV8QJcsk1o4T7j/OCtwHn82sXQCYwqGWaADLdS1VnM3Q7GS
groups:
- EDM
- Queendom
- Chat
- Author
displayName: Janine Sanders
attributes:
img: /var/www/html/starportraets/profiles/JanineSanders.jpg
imgfile: JanineSanders.jpg
email: janine_sanders@dieckmann.cloud
chatname: janine_sanders@jabber.hot-chilli.net
chatpassword: techno2004
favourites:
- DJ Mandy
- Katja Krasavice
- Gwen Stefani
- Katy Perry
- Rihanna
- Lady Gaga
- La Bouche
- Technotronic
- Ava Max
- Dua Lipa
- The Pussycat Dolls
- Lady Faith
- Ace of Base
- Adrima
- Alexandra Stan
- Paris Hilton
- Lari Luke
- "Badm\xF3mzjay"
- Brooklyn Bounce
detlef:
pwhash: $2y$15$od4hVrCSQOxuy8wwv4F8Y.PmyvMj0wQYPaTkfrWPiJcGVbtBRArGa
groups:
- EDM
- Queendom
- Chat
- Author
displayName: Detlef Dieckmann
attributes:
img: /var/www/html/starportraets/profiles/DetlefDieckmann.jpg
email: detlef@dieckmann.cloud
imgfile: DetlefDieckmann.jpg
favourites:
- DJ Mandy
- Katja Krasavice
- Gwen Stefani
- Shari
- Babsi Tollwut
- Rinaly
- Fiva
- Layla
- Schwesta Ewa
- Nicki Minaj
- The Pussycat Dolls
- Lady Faith
- Alice Dee
- Adrima
- Alexandra Stan
- Paris Hilton
- Lari Luke
- "Badm\xF3mzjay"
- Brooklyn Bounce
- Lena Stoehrfaktor
- BountyCocoa
- Eva Shaw
- Ida Corr
- Nina Chuba
- Nura
- SXTN
- Shirin David
- Chan Le
- Hanna Noir
- Satarii
- Nifra
- Lil Kim
- Die P
- Celo Minati
- Juju
- BIA
- Ace Tee
- Nashi44
- Ikkimel
- Iggy Azalea
...
Wenn die Nextcloud die E-Mail-Adresse des Users und dessen verschlüsseltes Passwort mitseinden würde, dann könnte ich über PHP etwas machen.
ChatGPT-4.0 • 2025-08-10 11:08:54
Super, das hilft! Da du dein Musikportal auf einem stark erweiterten **Pico CMS** betreibst und aktuell eine **LocalAuth.yml** nutzt, ist der sauberste Weg: **Nextcloud als OpenID-Connect-IdP** und dein Pico-Portal als OIDC-Client. Damit brauchst du weder Passwörter noch Hashes von Nextcloud (die bekommst du aus Sicherheitsgründen auch nicht) – du bekommst stattdessen **signierte ID-Tokens** mit E-Mail, Name usw., und mapst damit auf deine lokalen User. citeturn2view0turn0search10
Kurz gesagt: **Kein Passwort-Sharing.** Stattdessen **SSO via OIDC** + **User-Mapping per E-Mail (oder `sub`)**.
---
# Fahrplan
1) **Nextcloud als OIDC-Provider aktivieren**
- App „**OIDC Identity Provider**“ installieren/aktivieren. citeturn2view0
- In Nextcloud einen **Client** für dein Musikportal anlegen (Redirect-URI z. B. `https://music.deine-domain.tld/auth/callback`).
- Scopes: mindestens `openid email profile`. (OIDC ist in der Admin-Doku beschrieben.) citeturn0search10
2) **Pico-Portal als OIDC-Client**
Variante A (Plugin/Lib): Nutze ein OIDC/OAuth2-fähiges Auth-Plugin oder eine PHP-Lib (z. B. `jumbojett/openid-connect-php` oder ähnliches – Standardlibs sind reichlich vorhanden). citeturn0search18
Variante B (dein Code):
- `/login` leitet zum Nextcloud **/authorize** um (Code-Flow).
- `/auth/callback` tauscht `code` gegen **Access Token** + **ID Token**, validiert die **JWT** via Nextcloud-JWKs (Discovery-Endpoint).
- Danach **Userinfo** abholen (OIDC „userinfo“-Endpoint) und Claims wie `email`, `name` lesen. citeturn0search10
3) **User-Mapping in deinem System**
- **Primärer Schlüssel**: `email` (oder `sub` vom IdP; `sub` ist unveränderlich, E-Mail kann sich ändern).
- Beim ersten Login:
- Suche in deiner `LocalAuth.yml` nach passender `attributes.email`.
- Wenn **gefunden**: speichere eine Zuordnung `{ sub -> lokaler_username }` in einer kleinen Mapping-Tabelle/Datei (z. B. `oidc_map.json`) – so bist du robust gegen spätere E-Mail-Änderungen.
- Wenn **nicht gefunden**: **Auto-Provisioning** – lege einen neuen Eintrag an (siehe unten), oder zwinge zum ersten Mal eine Profilseite, wo der User seinen Anzeigenamen bestätigt.
- **Gruppen**: Wenn der Nextcloud-OIDC-Provider Gruppen als Claim liefert, kannst du sie übernehmen; sonst vergibst du Default-Gruppen oder pflegst sie lokal. (Gruppen-Handling hängt von der OIDC-App/Scopes ab.) citeturn0search10
4) **LocalAuth.yml anpassen (token-first, passwortfrei)**
Du kannst Passwörter für OIDC-User komplett vermeiden und nur noch für Alt-Logins nutzen. Beispiel für Auto-Provisioning eines neuen Users auf Basis der OIDC-Claims:
```yaml
users:
detlef:
# pwhash optional leer oder ein Platzhalter; Login läuft über OIDC
pwhash: ''
groups:
- EDM
- Queendom
- Chat
- Author
displayName: "Detlef Dieckmann"
attributes:
email: "detlef@dieckmann.cloud"
img: "/var/www/html/starportraets/profiles/DetlefDieckmann.jpg"
imgfile: "DetlefDieckmann.jpg"
favourites: [ ... ]
# separate Datei (JSON) für OIDC-Mapping:
# { "oidc_sub:abcd1234": "detlef" }
```
> Hinweis: In deiner YAML steckt z. T. sensibles Zeug (z. B. `chatpassword`). Lege solche Secrets besser **nicht** im Klartext ab – oder trenne sie in eine **.env / Vault** aus und injiziere sie nur zur Laufzeit.
---
# Minimaler PHP-Flow (Skizze)
- **Login-Start** (`/login`): Redirect auf
`https://nextcloud.example/.well-known/openid-configuration` auslesen → `authorization_endpoint`, dann mit `client_id`, `redirect_uri`, `scope=openId email profile`, `state`, `code_challenge` (PKCE) weiterleiten. citeturn0search10
- **Callback** (`/auth/callback`):
1) `code` → Token-Endpoint tauschen (Backchannel, `client_secret`/PKCE).
2) `id_token` **prüfen** (Signatur via JWKs, `aud`, `iss`, `exp`, `nonce`).
3) `email` + `sub` aus den Claims ziehen (oder via `userinfo`-Endpoint mit Access-Token).
4) **Mapping**:
- `sub` in `oidc_map.json` → lokaler Benutzername
- sonst Mail-Lookup in `LocalAuth.yml` → verknüpfen oder Auto-Provisioning
5) Eigene Session setzen → fertig eingeloggt.
Für die Serverseite ist dieser Ansatz „Standard-OIDC“ und passt gut zu Nextclouds OIDC-IdP. citeturn0search10turn0search1
---
# Warum nicht „E-Mail + Passwort-Hash mitgeben“?
- Nextcloud gibt **keine** (und sollte niemals) **Passwörter/Hashes** an Drittsysteme weiter – das wäre ein Anti-Pattern und ein Sicherheitsrisiko.
- OIDC löst genau dieses Problem: Du bekommst **verifizierbare Identität** (ID-Token), **ohne** das Passwort zu sehen. citeturn0search10
---
Wenn du willst, bastel ich dir im nächsten Schritt einen **konkreten PHP-Callback** (mit `jumbojett/openid-connect-php` oder `league/oauth2-client`) inkl. Beispiel, wie du deine `LocalAuth.yml` lädst/aktualisierst und eine `oidc_map.json` pflegst.
Detlef • 2025-08-10 11:23:39
In der Loginmaske des Musikportals selbst habe ich einen Button "Nextcloud" bereits integriert. Mein PHP redirect dann zur Nextcloud und zum Consent-Screen des IDP. Diese Richtung funktioniert bereits einwandfrei. Ich kann mich also direkt im Musikportal einloggen oder hierfür die Nextcloud als IDP nutzen.
Die andere Richtung funktioniert nicht! Wenn ich also bereits in der Nextcloud angemeldet bin und über einen Button der App "external" das Musikportal aufrufe, dann muss ich mich im Musikportal anmelden.
ChatGPT-4.0 • 2025-08-10 11:23:53
Alles klar – du hast also bereits **SP-initiiertes OIDC** vom Musikportal zur Nextcloud am Laufen (Button „Nextcloud“ ⇒ Redirect zum IdP ⇒ klappt).
Was dir fehlt, ist die **andere Richtung**: Wenn du *aus Nextcloud heraus* (App „External“) dein Musikportal öffnest, sollst du **ohne weiteren Klick** schon drin sein.
Kurzfassung: Bau im Musikportal eine **Auto-SSO-Schicht**, die bei *jedem ersten Aufruf* prüft „bin ich eingeloggt?“ – wenn **nein**, dann sofort den **OIDC-Flow starten** (ohne Button), idealerweise mit `prompt=none`. Ist beim IdP (Nextcloud) schon eine Session vorhanden, bekommst du **ohne Maske** ein ID-Token zurück; nur falls keine IdP-Session vorhanden ist, fällst du sauber auf deine normale Loginseite zurück. citeturn0search18
---
# So geht’s konkret
## 1) Musikportal: „SSO-Guard“ (Middleware)
Vor dein Pico-CMS (oder im Front-Controller) eine Middleware schalten:
- Wenn **keine** Portal-Session vorhanden:
1. Redirect auf den **Authorization-Endpoint** deines Nextcloud-IdP mit:
- `response_type=code`
- `scope=openid email profile`
- `prompt=none` ← versucht „silent“ zu authentifizieren, **ohne** UI
- `redirect_uri=https://music.example/auth/callback`
- `state` und `nonce` (CSRF/Replay-Schutz)
2. Falls der IdP mit `error=login_required` oder `consent_required` antwortet → **kein** Endlosschleifen-Redirect! Dann **normal** deine Loginseite zeigen (mit deinem „Nextcloud“-Button).
3. Wenn Tokens kommen: **ID-Token prüfen** (Signatur via JWKs; `iss`, `aud`, `exp`, `nonce`) und den User **einloggen** (Mapping, siehe unten).
(Mit z. B. `jumbojett/openid-connect-php` ist das in wenigen Zeilen erledigt.) citeturn0search2turn0search6turn0search10
➡️ Ergebnis: Wird dein Portal über **Nextcloud/External** geöffnet, startet der OIDC-Flow **automatisch**. Ist der User dort bereits angemeldet, landet er ohne weiteren Klick und ohne Loginmaske direkt im Portal.
## 2) Consent „wegklicken“
„Silent“ funktioniert nur, wenn für diesen Client **kein interaktiver Consent** notwendig ist. Üblich ist:
- **Client einmalig authorisieren** und „merken“ lassen, oder
- Den Client als **vertrauenswürdig** konfigurieren (falls dein IdP das anbietet).
Viele OIDC-IdPs haben dafür eine Einstellung; `prompt=none` fordert an, *ohne* UI durchzulaufen – wenn dennoch Consent nötig ist, kommt `consent_required`. (Dann greift dein Fallback auf die normale Loginmaske.) citeturn0search24turn0search18
## 3) Nextcloud External Sites: Iframe vs. neuer Tab
- Wenn die externe Seite **im Iframe** geladen wird, achte auf **Cookies/SameSite**:
- Setze deine **Portal-Session-Cookies** auf `SameSite=None; Secure`, sonst verschwinden sie im Iframe.
- Manche Browser blockieren Third-Party-Cookies im Iframe – im Zweifel **„in neuem Tab öffnen“** konfigurieren.
- Die External-Sites-App macht **keinen** SSO-Magie-Sync; sie ist nur ein Link-Container. Die SSO-Logik muss auf der **Portal-Seite** passieren. citeturn0search1
## 4) User-Mapping (deine LocalAuth.yml weiter nutzen)
- Identifiziere Benutzer primär über **`sub`** (stabil) oder **`email`** aus dem **ID-Token** / `userinfo`.
- Vorgehen:
1. Lookup: `sub` in `oidc_map.json` ⇒ liefert z. B. `detlef` (dein lokaler Username).
2. Falls nicht vorhanden: Suche `email` in `LocalAuth.yml` ⇒ verknüpfe und schreibe `{ "sub":"lokaler_user" }` in `oidc_map.json`.
3. Falls weder `sub`-Mapping noch `email` gefunden: **Auto-Provisioning** (neuen YAML-Block erzeugen) oder auf Setup-Seite führen.
- Passwortfelder in YAML sind für OIDC-User **überflüssig**; du kannst sie leer lassen – Auth geschieht über das **geprüfte ID-Token**.
(Die Nextcloud gibt dir *keine* Passwörter oder Hashes – und das ist auch gut so.) citeturn0search10
## 5) Stolpersteine & Fixes
- **Redirect-Loop**: Achte darauf, `prompt=none` **nur** einmal pro Besuch zu versuchen (z. B. per kurzlebigem Flag im Querystring oder in `sessionStorage`), und falle dann auf die Loginseite zurück.
- **CSP/X-Frame-Options**: Wenn du iframest, erlaube Einbettung aus deiner Nextcloud-Origin (`frame-ancestors https://cloud.example`).
- **Zeitdrift**: OIDC-`exp` ist streng; stelle Serverzeit per NTP sicher.
- **Scopes/Claims**: Für E-Mail-Mapping `scope=email` und Claim `email` sicherstellen.
- **Discovery/JWKs**: Nutze den **Well-Known**-Endpoint der Nextcloud-IdP-App; dort findest du `authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`, `jwks_uri`. citeturn0search10
---
## Mini-Flow (Pseudocode)
```php
// SSO-Guard am Anfang jeder Request-Chain:
if (!isLoggedIn()) {
if (!isset($_GET['sso']) || $_GET['sso'] !== 'tried') {
oidcAuthorizeRedirect([
'prompt' => 'none',
'state' => makeState(),
'nonce' => makeNonce(),
'scope' => 'openid email profile',
'extra' => ['sso' => 'tried'], // kommt als Query im state/redirect wieder
]);
exit;
}
// silent login hat nicht geklappt -> zeige normale Loginseite
showLoginPage(); exit;
}
// /auth/callback:
$tokens = exchangeCodeForTokens($_GET['code']);
$id = validateIdToken($tokens['id_token']); // Signatur, iss, aud, exp, nonce
$user = mapSubOrEmailToLocalUser($id['sub'], $id['email']);
login($user);
redirectToHome();
```
Mit `jumbojett/openid-connect-php` kannst du dir einiges davon sparen; die Lib bringt Discovery, Token-Tausch und Validierung mit. citeturn0search2
---
## Alternative (fortgeschritten): Nextcloud AppAPI „External App Auth“
Wenn du dein Portal enger an Nextcloud binden willst, wirf einen Blick auf das **AppAPI-Auth-Modell** (Shared-Secret-basierte Auth für externe Apps). Das ist aber ein anderer Weg als OIDC und erfordert mehr Nextcloud-seitige Integration. Für deinen Fall ist **OIDC-Silent-Login** deutlich geradliniger. citeturn0search23
---
Wenn du magst, bastle ich dir eine **konkrete PHP-Implementierung** mit `jumbojett/openid-connect-php` (inkl. `composer.json`, Callback-Route, `oidc_map.json`-Logik und einem kleinen YAML-Helper), die du direkt in dein Pico-Setup hängen kannst.
Detlef • 2025-08-10 11:33:27
Hier einmal mein login.twig:
{% extends "picoAuth_base.twig" %}
{% set viewportClass = 'compact' %}
{% block body %}
{% autoescape %}
{% include 'partials/flashes.twig' %}
<div class="shadow">
{% if auth.plugin.module("localAuth") %}
<div class="box">
<div class="box-title" style="border-top-left-radius:15px;border-top-right-radius:15px;">Anmeldung</div>
<div class="box-content">
<form id="frmLogin" method="POST" action="{{ "login"|link }}">
{{ csrf_field("login") }}
<div>
<i class="fa fa-user icon"></i>
<input class="input-field" type="text" id="login_username" name="username" placeholder="Benutzername" required >
</div>
<div>
<i class="fa fa-key icon"></i>
<input class="input-field" type="password" id="login_pass" name="password" placeholder="Passwort" autocomplete="off" required>
</div>
<div class="formBar">
<div style="text-align:center;">
<button type="submit">
Login
</button>
</div>
<div>
{% if auth.plugin.getModule("localAuth").config.registration.enabled %}
<a href="{{ "register"|link }}">Register an account</a>
{% endif %}
{% if auth.plugin.getModule("localAuth").config.passwordReset.enabled %}
<a href="{{ "password_reset"|link }}">Forgot your password?</a>
{% endif %}
</div>
</div>
</form>
</div>
</div>
{% endif %}
{% if auth.plugin.module("OAuth") %}
<div class="box">
<div class="box-title">Anmeldung über NeonVibes</div>
</div>
<div class="box-content">
<form method="POST" action="{{ "login"|link }}">
{{ csrf_field("OAuth") }}
{% for provider in auth.plugin.getModule("OAuth").storage.providerNames %}
<button value="{{ provider }}" name="oauth" style="margin-top:3%;margin-bottom:3%;">{{ provider }}</button>
{% else %}
Keine SSO-Provider verfügbar.
{% endfor %}
</form>
</div>
</div>
{% endif %}
{% if not auth.plugin.module("localAuth") and not auth.plugin.module("OAuth") %}
<div class="box">
<div class="box-title">Login</div>
<div class="box-content">
Kein Authentifizierungsverfahren verfügbar.
</div>
</div>
{% endif %}
{% endautoescape %}
{% endblock %}
ChatGPT-4.0 • 2025-08-10 11:33:29
Top, danke fürs Snippet! Du hast also schon OIDC vom Portal → Nextcloud (per Button) am Start. Für den Weg **Nextcloud → Portal** brauchst du jetzt ein „Silent-Login“, das **automatisch** anspringt, sobald jemand aus Nextcloud (App „External“) bei dir landet.
Hier ist ein pragmatischer Plan, der ohne große Umbauten an deinem Auth-Stack funktioniert und sauber fallbackt:
---
## 1) External-Link in Nextcloud anpassen
Setz in der Nextcloud-App „External“ als URL z. B.:
```
https://music.example.tld/login?auto=1
```
(oder direkt die Zielseite, **wichtig ist nur `?auto=1`**).
Damit weiß dein Portal: „Versuch beim ersten Aufruf einen **silent OIDC-Login**.“
---
## 2) Serverseitig: „Silent“ nur einmal versuchen
Idealerweise machst du das **serverseitig**, bevor die Loginseite gerendert wird:
Pseudocode (Front-Controller / Middleware vor `login.twig`):
```php
session_start();
$auto = isset($_GET['auto']) && $_GET['auto'] === '1';
if ($auto && !isLoggedIn() && oidcIsConfigured()) {
// Silent-Login anfordern, ohne UI
// -> prompt=none, mit state/nonce; bei login_required => zurück auf /login (ohne auto=1)
oidcAuthorizeRedirect([
'scope' => 'openid email profile',
'prompt' => 'none',
'state' => makeState(['tried' => 1]),
'nonce' => makeNonce(),
'redirect_uri' => 'https://music.example.tld/auth/callback'
]);
exit;
}
// Wenn wir hier sind: entweder bereits eingeloggt ODER silent hat nicht geklappt (consent/login_required) -> normale Loginseite rendern.
```
Vorteil: Kommt der Nutzer aus Nextcloud und hat **dort bereits eine Session**, landet er **ohne weiteren Klick** direkt bei dir drin. Nur wenn der IdP *trotzdem* eine Maske/Consent will, fällst du sauber auf deine bestehende Loginseite zurück.
---
## 3) (Optional) Twig-Fallback: Auto-POST des OAuth-Buttons
Falls du (erstmal) **nichts** am PHP-Flow ändern willst, kannst du die bestehende Loginseite minimal „boosten“:
Wenn `?auto=1` anliegt **und** der OIDC-Provider vorhanden ist, **postest du einmalig automatisch** das vorhandene OAuth-Formular (mit CSRF-Token!) – ohne dass jemand den Button klicken muss.
### Patch für dein `login.twig`
Füge **unterhalb** deines OAuth-Blocks Folgendes ein:
```twig
{% if auth.plugin.module("OAuth") %}
{# ... dein bestehender OAuth-Block bleibt wie er ist ... #}
{# 1) Auto-Trigger, wenn ?auto=1 in der URL steht #}
{% set auto = app.request.get('auto') ?? app.request.query.get('auto') ?? null %}
{# 2) Ersten Provider ermitteln #}
{% set firstProvider = auth.plugin.getModule("OAuth").storage.providerNames|first %}
{# 3) Unsichtbares Formular, das den ersten Provider postet #}
{% if auto == '1' and firstProvider %}
<form id="frmAutoOAuth" method="POST" action="{{ "login"|link }}" style="display:none;">
{{ csrf_field("OAuth") }}
<input type="hidden" name="oauth" value="{{ firstProvider }}">
</form>
<script>
(function() {
try {
// nur EINMAL auto-try pro Seite (gegen Loops)
if (!sessionStorage.getItem('sso_tried')) {
sessionStorage.setItem('sso_tried', '1');
document.getElementById('frmAutoOAuth').submit();
}
} catch(e) {}
})();
</script>
{% endif %}
{% endif %}
```
**Was passiert?**
- Kommt der Nutzer via `...?auto=1`, wird dein vorhandenes OAuth-Login **einmalig automatisch** abgesendet (CSRF-safe).
- Ist der User in Nextcloud schon eingeloggt **und** der Client bereits autorisiert, ist er sofort drin.
- Falls der IdP doch UI/Consent will, sieht er deine normale Loginmaske (kein Loop, weil `sessionStorage` blockt ein zweites Auto-Submit).
> Hinweis: `app.request...` kann je nach Pico/Twig-Setup anders heißen. Wenn du keinen Request-Zugriff im Template hast, kannst du die `auto`-Info auch serverseitig als Twig-Variable setzen (z. B. `$twig->render('login.twig', ['auto' => $_GET['auto'] ?? null]);`) und im Twig dann einfach `auto` verwenden.
---
## 4) Consent & „Trusted Client“
Damit **silent** wirklich ohne UI klappt, sollte dein Nextcloud-IdP den Client als „gemerkt“/vertrauenswürdig behandeln (oder der User hat bereits einmal zugestimmt). Ansonsten liefert der IdP `consent_required` und dein Fallback greift – das ist ok.
---
## 5) Iframe-Themen (wichtig!)
Wenn die External-App dein Portal **im Iframe** lädt:
- **Cookies**: Setz deine PHP-Session auf **`SameSite=None; Secure`**, sonst werden Cookies im Iframe nicht gespeichert.
```php
session_set_cookie_params([
'secure' => true,
'httponly' => true,
'samesite' => 'None',
]);
session_start();
```
- **Framing erlauben**:
- `Content-Security-Policy: frame-ancestors https://deine-nextcloud.tld;`
- **kein** `X-Frame-Options: DENY` (oder setz `SAMEORIGIN` passend).
- **3rd-Party-Cookies**: Einige Browser blocken sie im Iframe – falls’s hakt, stell in Nextcloud „im neuen Tab öffnen“ ein.
---
## 6) Mapping bleibt wie gehabt
Im Callback:
- `sub` und/oder `email` aus dem ID-Token/`userinfo` ziehen,
- `sub → lokaler_username` in `oidc_map.json` pflegen,
- bei Erstlogin via `email` in deiner `LocalAuth.yml` matchen oder auto-provisionieren,
- lokale Passwörter für OIDC-User sind nicht nötig.
---
## 7) Typische Stolpersteine
- **Redirect-Loop**: Der obige `sessionStorage`-Schutz verhindert’s im Twig-Fallback. Serverseitig: `prompt=none` nur einmal pro Visit versuchen.
- **Zeitdrift**: OIDC-`exp` ist streng – NTP checken.
- **CSRF**: Du nutzt bereits `csrf_field("OAuth")` – gut! Beim Auto-POST bleibt das erhalten.
- **Scope/Claims**: `openid email profile` nutzen, wenn du `email` zum Mappen brauchst.
---
Wenn du magst, gebe ich dir noch die **konkreten PHP-Stubs** für:
- `oidcAuthorizeRedirect(prompt=none)` inkl. PKCE/State/Nonce,
- `auth/callback` (Code-Tausch, ID-Token-Verify, Mapping),
- `session_set_cookie_params` für Iframe-Kompatibilität,
- sowie ein kleines `YamlUserStore`-Helperlein (lesen/schreiben `LocalAuth.yml`) plus `oidc_map.json`.
Detlef • 2025-08-10 11:42:18
Den Get-Parameter ?auto=1 habe ich an die URL angehängt. Es gibt keine IFrames. Das Musikportal wird in einer neuen Seite geöffnet.
ChatGPT-4.0 • 2025-08-10 11:42:21
Perfekt, dann brauchst du „silent OIDC“ beim **ersten Aufruf** – ohne iFrame-Sonderfälle. Du hast `?auto=1` schon dran; jetzt sorgst du serverseitig dafür, dass bei `auto=1` **sofort** der Authorization-Flow mit `prompt=none` startet und – falls Nextcloud-Session vorhanden & Consent erteilt – ohne weitere UI zurückkommt.
Hier ist ein kompakter, praxistauglicher Weg:
---
# 1) Guard vor dem Rendern der Loginseite
In deinem Front-Controller bzw. vor dem Rendern von `login.twig`:
```php
// bootstrap.php oder vor Renden der Login-View
session_start();
function isLoggedIn(): bool {
return isset($_SESSION['user']);
}
function oidcIsConfigured(): bool {
return is_file(__DIR__ . '/oidc_config.php'); // oder echte Prüfung
}
$auto = isset($_GET['auto']) && $_GET['auto'] === '1';
// "silent login" nur beim ersten Besuch versuchen, um Loops zu vermeiden:
if ($auto && !isLoggedIn() && oidcIsConfigured() && empty($_SESSION['silent_tried'])) {
$_SESSION['silent_tried'] = 1;
// Redirect zum IdP mit prompt=none, PKCE optional aber empfohlen
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$_SESSION['oidc_state'] = $state;
$_SESSION['oidc_nonce'] = $nonce;
// Discovery lesen (einmalig cachen) – hier kurz hart verdrahtet:
$authorize = 'https://cloud.example.tld/apps/oidc/authorize'; // oder aus .well-known
$clientId = 'musicportal';
$redirect = 'https://music.example.tld/auth/callback'; // exakt wie im IdP registriert
$scope = 'openid email profile';
$params = http_build_query([
'response_type' => 'code',
'client_id' => $clientId,
'redirect_uri' => $redirect,
'scope' => $scope,
'state' => $state,
'nonce' => $nonce,
'prompt' => 'none', // <-- silent
]);
header('Location: ' . $authorize . '?' . $params);
exit;
}
// Wenn wir hier sind: entweder eingeloggt ODER silent hat nicht geklappt.
// -> normale Loginseite rendern (dein login.twig).
```
**Ergebnis:** Wenn der Aufruf aus Nextcloud kommt (`?auto=1`), startet sofort der OIDC-Flow. Ist der User in Nextcloud schon angemeldet **und** der Client einmalig autorisiert, landet er ohne weiteren Klick im Portal.
---
# 2) Callback-Endpoint
Der Callback tauscht `code` gegen Tokens, prüft das ID-Token (Signatur + `iss|aud|exp|nonce`) und mappt `sub`/`email` auf deinen lokalen User.
Beispiel mit der verbreiteten Lib **jumbojett/openid-connect-php** (macht Discovery, Token-Tausch und Prüfung für dich):
```php
// composer require jumbojett/openid-connect-php
// auth/callback.php
session_start();
require __DIR__ . '/vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient(
'https://cloud.example.tld', // Issuer Base URL (Discovery findet Endpunkte)
'musicportal', // client_id
'SUPERSECRET' // client_secret (oder PKCE-only ohne Secret)
);
$oidc->setRedirectURL('https://music.example.tld/auth/callback');
$oidc->addScope(['openid','email','profile']);
// Optional: Nonce/State prüfen, wenn du sie selbst verwaltest
// $oidc->setState($_SESSION['oidc_state'] ?? null);
// $oidc->setNonce($_SESSION['oidc_nonce'] ?? null);
// Tauscht code, validiert ID Token
$oidc->authenticate(); // wirft Exception bei Fehler
$idToken = $oidc->getIdToken();
$claims = $oidc->requestUserInfo(); // oder aus $oidc->getVerifiedClaims()
$sub = $claims->sub ?? null;
$email = $claims->email ?? null;
$name = $claims->name ?? ($claims->preferred_username ?? null);
// --- Mapping auf deine LocalAuth.yml ---
$localUser = mapSubOrEmailToLocalUser($sub, $email); // eigene Funktion:
// 1) in oidc_map.json sub->username nachschlagen
// 2) sonst email in LocalAuth.yml finden, dann Mapping sub->username persistieren
// 3) sonst (optional) auto-provisionieren
if (!$localUser) {
// Fallback: Setup-Seite oder Fehlermeldung
header('Location: /login?error=nomap'); exit;
}
// Erfolgreich -> eigene Session setzen
$_SESSION['user'] = $localUser;
// Aufräumen
unset($_SESSION['silent_tried'], $_SESSION['oidc_state'], $_SESSION['oidc_nonce']);
// Zurück zur Startseite (oder gewünschte Zielseite)
header('Location: /'); exit;
```
> Wenn du lieber **PKCE ohne Client-Secret** fährst: mit `jumbojett` geht das auch; ansonsten kannst du den Code-Tausch und die Signaturprüfung „von Hand“ machen (JWT prüfen via JWKs, `iss/aud/exp/nonce` checken).
---
# 3) Wichtig für „silent“:
- **Consent**: „Silent“ klappt nur, wenn der Client **schon autorisiert** ist oder in Nextcloud als **vertrauenswürdig** gilt. Einmalig Consent geben, „merken“ lassen – dann läuft’s automatisch.
- **Loop-Schutz**: Das `$_SESSION['silent_tried']` verhindert, dass du bei `login_required/consent_required` in eine Redirect-Schleife kommst. Danach siehst du einfach deine normale Loginseite.
- **State/Nonce**: Wie oben gezeigt generieren und prüfen (Lib übernimmt viel, aber halte die Werte parat).
---
# 4) Dein `login.twig` musst du nicht anfassen
Da du **serverseitig** vor dem Rendern entscheidest, ob ein silent-Redirect losgeht, bleibt dein Twig so wie er ist. Der Button „Nextcloud“ bleibt als manueller Fallback bestehen.
---
# 5) Mini-Mapping-Skizze
```php
function mapSubOrEmailToLocalUser(?string $sub, ?string $email): ?string {
// 1) sub -> username
$mapFile = __DIR__ . '/oidc_map.json';
$map = is_file($mapFile) ? json_decode(file_get_contents($mapFile), true) : [];
if ($sub && isset($map[$sub])) return $map[$sub];
// 2) email in LocalAuth.yml finden
$yaml = yaml_parse_file(__DIR__ . '/LocalAuth.yml');
foreach ($yaml['users'] ?? [] as $username => $data) {
if (($data['attributes']['email'] ?? null) === $email) {
if ($sub) { $map[$sub] = $username; file_put_contents($mapFile, json_encode($map, JSON_PRETTY_PRINT)); }
return $username;
}
}
// 3) optional: Auto-Provisioning hier
return null;
}
```
---
Wenn du mir kurz sagst, wie dein Routing/Front-Controller aufgebaut ist, passe ich dir den **Guard** und den **Callback** exakt in deine Struktur (Pico-Hook, Plugin oder simples `index.php`).
Detlef • 2025-08-10 11:53:05
Meinst Du meine angepasste oauth.php des Plugins?
<?php
namespace PicoAuth\Module\Authentication;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
use League\OAuth2\Client\Provider\AbstractProvider;
use PicoAuth\Log\LoggerTrait;
use PicoAuth\Module\AbstractAuthModule;
use PicoAuth\Storage\Interfaces\OAuthStorageInterface;
use PicoAuth\Session\SessionInterface;
use PicoAuth\PicoAuthInterface;
use PicoAuth\User;
use PicoAuth\Utils;
use Psr\Log\LoggerAwareInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* OAuth 2.0 authentication
*
* Uses authorization code grant
*/
class OAuth extends AbstractAuthModule implements LoggerAwareInterface
{
use LoggerTrait;
use ArrayAccessorTrait;
const LOGIN_CSRF_ACTION = 'OAuth';
/**
* Instance of the plugin
*
* @var PicoAuthInterface
*/
protected $picoAuth;
/**
* Session manager
*
* @var SessionInterface
*/
protected $session;
/**
* Configuration storage
*
* @var OAuthStorageInterface
*/
protected $storage;
/**
* Configuration array
*
* @var array
*/
protected $config;
/**
* OAuth 2.0 provider used
*
* @var AbstractProvider
*/
protected $provider;
/**
* Configuration array for the provider
*
* @var array
*/
protected $providerConfig;
public function __construct(
PicoAuthInterface $picoAuth,
SessionInterface $session,
OAuthStorageInterface $storage
) {
$this->picoAuth = $picoAuth;
$this->session = $session;
$this->storage = $storage;
$this->config = $this->storage->getConfiguration();
}
/**
* @inheritdoc
*/
public function getName()
{
return 'OAuth';
}
/**
* Can be used from twig template when dynamically compositing a login page
* @return OAuthStorageInterface
*/
public function getStorage()
{
return $this->storage;
}
/**
* @inheritdoc
*/
public function onPicoRequest($url, Request $httpRequest)
{
$post = $httpRequest->request;
//SSO login submission
if ($url === "login" && $post->has("oauth")) {
//CSRF validation
if (!$this->picoAuth->isValidCSRF($post->get("csrf_token"), self::LOGIN_CSRF_ACTION)) {
$this->picoAuth->redirectToLogin(null, $httpRequest);
}
$provider = $post->get("oauth");
// Find the requested provider (case sensitive)
$providerConfig = $this->storage->getProviderByName($provider);
if (!$providerConfig) {
$this->session->addFlash("error", "Requested provider is not available.");
$this->picoAuth->redirectToLogin(null, $httpRequest);
}
$this->initProvider($providerConfig);
$this->session->set("provider", $provider);
$this->saveAfterLogin($httpRequest);
// Starts with retreiving the OAuth authorization code
$this->startAuthentication();
} elseif ($url === $this->config["callbackPage"] && $this->isValidCallback($httpRequest)) {
// Request on the SSO endpoint
$provider = $this->session->get("provider");
$this->session->remove("provider");
$providerConfig = $this->storage->getProviderByName($provider);
if (!$providerConfig) {
$this->session->remove("oauth2state");
throw new \RuntimeException("Provider removed during auth process.");
}
$this->initProvider($providerConfig);
$this->finishAuthentication($httpRequest);
}
}
/**
* Initializes an instance of the provider from the configuration
*
* @param array $providerConfig Configuration array of the selected provider
* @throws \RuntimeException If the provider is not resolvable
*/
protected function initProvider($providerConfig)
{
$providerClass = $providerConfig['provider'];
$options = $providerConfig['options'];
if (!isset($options['redirectUri'])) {
// Set OAuth 2.0 callback page from the configuration
$options['redirectUri'] = $this->picoAuth->getPico()->getPageUrl($this->config["callbackPage"]);
}
if (!class_exists($providerClass)) {
throw new \RuntimeException("Provider class $providerClass does not exist.");
}
if (!is_subclass_of($providerClass, AbstractProvider::class, true)) {
throw new \RuntimeException("Class $providerClass is not a League\OAuth2 provider.");
}
$this->provider = new $providerClass($options);
$this->providerConfig = $providerConfig;
}
/**
* Starts the OAuth 2.0 process
*
* Redirects to the authorization URL of the selected provider
*/
protected function startAuthentication()
{
$authorizationUrl = $this->provider->getAuthorizationUrl();
$this->session->migrate(true);
$this->session->set("oauth2state", $this->provider->getState());
// The final redirect, halts the script
$this->picoAuth->redirectToPage($authorizationUrl, null, false);
}
/**
* Finishes the OAuth 2.0 process
*
* Handles the authorization code response to the authorization request
* Expects that the Request contains a valid callback, which should
* be checked by {@see OAuth::isValidCallback()}.
*
* @param Request $httpRequest
*/
protected function finishAuthentication(Request $httpRequest)
{
$sessionCode = $this->session->get("oauth2state");
$this->session->remove("oauth2state");
// Check that the state from OAuth response matches the one in the session
if ($httpRequest->query->get("state") !== $sessionCode) {
$this->onStateMismatch();
}
// Returns one of https://tools.ietf.org/html/rfc6749#section-4.1.2.1
if ($httpRequest->query->has("error")) {
$this->onOAuthError($httpRequest->query->get("error"));
}
// Error not set, but code not present (not an RFC complaint response)
if (!$httpRequest->query->has("code")) {
$this->onOAuthError("no_code");
}
try {
$accessToken = $this->provider->getAccessToken('authorization_code', [
'code' => $httpRequest->query->get("code"),
]);
$resourceOwner = $this->provider->getResourceOwner($accessToken);
$this->saveLoginInfo($resourceOwner);
} catch (IdentityProviderException $e) {
$this->onOauthResourceError($e);
}
}
/**
* Gets an attribute from the resource owner
*
* @param string $name Attribute name
* @param \League\OAuth2\Client\Provider\ResourceOwnerInterface $resourceOwner Resource owner instance
* @return mixed The retrieved value
*/
protected function getResourceAttribute($name, $resourceOwner)
{
// Call resource owner getter first
$method = "get" . $name;
if (is_callable(array($resourceOwner, $method))) {
$res = $resourceOwner->$method();
return $res;
} else {
$resourceArray = $resourceOwner->toArray();
$res = $this->getValueByKey($resourceArray, $name);
return $res;
}
}
/**
* Saves the information from the ResourceOwner
*
* @param \League\OAuth2\Client\Provider\ResourceOwnerInterface $resourceOwner
*/
protected function saveLoginInfo($resourceOwner)
{
// Initialize the user
$u = new User();
$u->setAuthenticated(true);
$u->setAuthenticator($this->getName());
// Get user id from the Resource Owner
$attrMap = $this->providerConfig['attributeMap'];
$userIdAttr = $attrMap['userId'];
$userId = $this->getResourceAttribute($userIdAttr, $resourceOwner);
$u->setId($userId);
unset($attrMap['userId']);
// Get display name from the Resource Owner (if configured)
if (isset($attrMap['displayName'])) {
$name = $this->getResourceAttribute($attrMap['displayName'], $resourceOwner);
$u->setDisplayName($name);
unset($attrMap['displayName']);
}
// Retrieve all other custom attributes from the attributeMap
foreach ($attrMap as $mapKey => $mapValue) {
$value = $this->getResourceAttribute($mapValue, $resourceOwner);
$u->setAttribute($mapKey, $value);
}
// Set default droups and default attributes
$u->setGroups($this->providerConfig['default']['groups']);
foreach ($this->providerConfig['default']['attributes'] as $key => $value) {
if (null === $u->getAttribute($key)) {
$u->setAttribute($key, $value);
}
}
$this->picoAuth->setUser($u);
$this->picoAuth->afterLogin();
}
/**
* Saves the afterLogin parameter
*
* @param Request $httpRequest
*/
protected function saveAfterLogin(Request $httpRequest)
{
$referer = $httpRequest->headers->get("referer", null, true);
$afterLogin = Utils::getRefererQueryParam($referer, "afterLogin");
if ($afterLogin && Utils::isValidPageId($afterLogin)) {
$this->session->set("afterLogin", $afterLogin);
}
}
/**
* Checks if the request and session have all the required fields for a OAuth 2.0 callback
*
* - The session must have "provider" name which is the provider the callback
* would be returned from.
* - The request must have "state" query param as a CSRF prevention.
* - The session must have "oauth2state" which must be a string
* a must be non-empty.
*
* @param Request $httpRequest
* @return bool true if the required fields are present, false otherwise
*/
protected function isValidCallback(Request $httpRequest)
{
return $this->session->has("provider")
&& $httpRequest->query->has("state")
&& $this->session->has("oauth2state")
&& is_string($this->session->get("oauth2state"))
&& (strlen($this->session->get("oauth2state")) > 0);
}
/**
* Logs invalid state in the OAuth 2.0 response
*/
protected function onStateMismatch()
{
$this->logger->warning(
"OAuth2 response state mismatch: provider: {provider} from {addr}",
array(
"provider" => get_class($this->provider),
"addr" => $_SERVER['REMOTE_ADDR']
)
);
$this->session->remove("oauth2state");
$this->session->addFlash("error", "Invalid OAuth response.");
$this->picoAuth->redirectToLogin();
}
/**
* On an OAuth error
*
* @param string $errorCode
*/
protected function onOAuthError($errorCode)
{
$errorCode = strlen($errorCode > 100) ? substr($errorCode, 0, 100) : $errorCode;
$this->logger->notice(
"OAuth2 error response: code {code}, provider {provider}",
array(
"code" => $errorCode,
"provider" => get_class($this->provider),
)
);
$this->session->addFlash("error", "The provider returned an error ($errorCode)");
$this->picoAuth->redirectToLogin();
}
/**
* On a Resource owner error
*
* @param IdentityProviderException $e
*/
protected function onOauthResourceError(IdentityProviderException $e)
{
$this->logger->critical(
"OAuth2 IdentityProviderException: {e}, provider {provider}",
array(
"e" => $e->getMessage(),
"provider" => get_class($this->provider),
)
);
$this->session->addFlash("error", "Failed to get an access token or user details.");
$this->picoAuth->redirectToLogin();
}
}
Die Bahuma\OAuth2\Client\Provider/Nextcloud.php habe ich auch angepasst:
<?php
namespace Bahuma\OAuth2\Client\Provider;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;
class Nextcloud extends AbstractProvider
{
use BearerAuthorizationTrait;
/**
* @var string Base URL of the nextcloud instance (not including trailing slash).
*/
protected $nextcloudUrl = '';
public function getBaseAuthorizationUrl()
{
return $this->nextcloudUrl . 'https://dieckmann.cloud/index.php/apps/oauth2/authorize';
}
public function getBaseAccessTokenUrl(array $params)
{
return $this->nextcloudUrl . 'https://dieckmann.cloud/index.php/apps/oauth2/api/v1/token';
}
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
return $this->nextcloudUrl . 'https://dieckmann.cloud/ocs/v2.php/cloud/user?format=json';
}
protected function getDefaultScopes()
{
return [
];
}
protected function checkResponse(ResponseInterface $response, $data)
{
// @codeCoverageIgnoreStart
if (empty($data['error'])) {
return;
}
// @codeCoverageIgnoreEnd
$code = 0;
$error = $data['error'];
if (is_array($error)) {
$code = $error['code'];
$error = $error['message'];
}
throw new IdentityProviderException($error, $code, $data);
}
protected function createResourceOwner(array $response, AccessToken $token)
{
return new NextcloudResourceOwner($response);
}
}
ChatGPT-4.0 • 2025-08-10 11:53:07
Ja, genau die meinte ich — danke fürs Teilen! Du hast damit alles an der richtigen Stelle, es fehlt nur ein kleines „Auto-Start“-Stück und zwei, drei Kanten sind in deiner `Nextcloud`-Providerklasse falsch/ungünstig. Hier ist, was ich ändern würde:
---
# 1) Silent-Redirect beim ersten Aufruf (`?auto=1`)
Dein Flow startet aktuell **nur** bei POST (`$post->has("oauth")`). Für den Weg **Nextcloud → Musikportal** brauchst du zusätzlich einen **GET-Trigger**, der bei `?auto=1` automatisch den gewünschten Provider setzt und den Authorization-Redirect startet – *ohne* Button.
### Patch in `onPicoRequest()` (oberhalb des POST-Blocks einfügen)
```php
public function onPicoRequest($url, Request $httpRequest)
{
$post = $httpRequest->request;
// 0) Auto-SSO: GET ?auto=1 -> einmalig Silent-Auth probieren
if ($url === "login" && $httpRequest->query->get("auto") === "1") {
// nur einmal pro Session, gegen Loops
if (!$this->session->has("silent_tried")) {
$this->session->set("silent_tried", 1);
// Provider wählen: entweder aus Config (empfohlen) oder erster Eintrag
$providerName = $this->config['autoProvider'] ?? ($this->storage->getProviderNames()[0] ?? null);
if ($providerName) {
$providerConfig = $this->storage->getProviderByName($providerName);
$this->initProvider($providerConfig);
$this->session->set("provider", $providerName);
// optional: afterLogin vom Referer merken
$this->saveAfterLogin($httpRequest);
// Silent-Login starten (prompt=none, siehe startAuthenticationSilent)
$this->startAuthenticationSilent();
return;
}
}
// Wenn kein Provider oder schon probiert: weiter zur normalen Loginseite
}
// 1) Vorhandener POST-Flow bleibt
if ($url === "login" && $post->has("oauth")) {
// ...
} elseif ($url === $this->config["callbackPage"] && $this->isValidCallback($httpRequest)) {
// ...
}
}
```
### Neue Methode in `OAuth` für „silent“
```php
protected function startAuthenticationSilent()
{
// Wie startAuthentication(), aber mit prompt=none
$authorizationUrl = $this->provider->getAuthorizationUrl([
'prompt' => 'none', // versucht ohne UI durchzulaufen, falls IdP-Session & Consent vorhanden
]);
$this->session->migrate(true);
$this->session->set("oauth2state", $this->provider->getState());
$this->picoAuth->redirectToPage($authorizationUrl, null, false);
}
```
> Wichtig: Dein `isValidCallback()` verlangt, dass `provider` **vorher** in die Session geschrieben wurde. Das mache ich oben mit `set("provider", $providerName)`.
---
# 2) Deine `Bahuma\OAuth2\Client\Provider\Nextcloud`-Klasse fixen
Aktuell baust du die URLs so:
```php
return $this->nextcloudUrl . 'https://dieckmann.cloud/index.php/apps/oauth2/authorize';
```
Das ergibt „`https://BASEhttps://dieckmann...`“. Stattdessen: `nextcloudUrl` = **Basis** inkl. Schema & Host (ohne Slash am Ende), die Pfade werden **angehängt**.
### Korrigierte Methoden
```php
protected $nextcloudUrl = ''; // z.B. 'https://dieckmann.cloud'
public function __construct(array $options = [], array $collaborators = [])
{
parent::__construct($options, $collaborators);
if (!empty($options['nextcloudUrl'])) {
$this->nextcloudUrl = rtrim($options['nextcloudUrl'], '/');
}
}
public function getBaseAuthorizationUrl()
{
return $this->nextcloudUrl . '/index.php/apps/oauth2/authorize';
}
public function getBaseAccessTokenUrl(array $params)
{
return $this->nextcloudUrl . '/index.php/apps/oauth2/api/v1/token';
}
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
// OCS User-Endpoint
return $this->nextcloudUrl . '/ocs/v2.php/cloud/user?format=json';
}
```
### OCS-Header ergänzen (wichtig!)
Der OCS-Endpoint will `OCS-APIRequest: true`. Füge die Header beim **authenticated request** hinzu:
```php
use Psr\Http\Message\RequestInterface;
protected function getAuthenticatedRequest($method, $url, $token, array $options = [])
{
$options['headers']['OCS-APIRequest'] = 'true';
$options['headers']['Accept'] = 'application/json';
// Authorization kommt aus BearerAuthorizationTrait
return parent::getAuthenticatedRequest($method, $url, $token, $options);
}
```
So liefert `getResourceOwner()` zuverlässig `ocs.data.*`.
---
# 3) Attribute-Mapping prüfen
Du nutzt:
```php
$attrMap = $this->providerConfig['attributeMap'];
$userIdAttr = $attrMap['userId']; // z.B. 'ocs.data.id' oder 'ocs.data.email'
```
Deine `getResourceAttribute()` greift über `ArrayAccessorTrait::getValueByKey()` zu – stelle sicher, dass deine `attributeMap` Keys zu Nextclouds OCS-Antwort passen, z. B.:
```json
{
"attributeMap": {
"userId": "ocs.data.id",
"displayName": "ocs.data.display-name",
"email": "ocs.data.email"
},
"default": {
"groups": ["EDM","Queendom","Chat","Author"],
"attributes": {}
},
"options": {
"nextcloudUrl": "https://dieckmann.cloud",
"clientId": "musicportal",
"clientSecret": "********",
"redirectUri": "https://music.example.tld/oauth/callback"
},
"provider": "Bahuma\\OAuth2\\Client\\Provider\\Nextcloud"
}
```
---
# 4) Consent & „silent“
- **Silent** (`prompt=none`) klappt nur, wenn:
- der User in Nextcloud **eingeloggt** ist **und**
- der Client **bereits autorisiert** wurde (Consent „merken“).
Falls Nextclouds OAuth-App keinen dauerhaften Consent speichert, bekommst du ein interaktives Prompt – dann greift dein Button-Fallback.
- Du hast jetzt Loop-Schutz via `silent_tried` (Session). Beim Fehlschlag siehst du normal `login.twig`.
---
# 5) Bonus-Checks
- **Zeit / State**: Du validierst `state` – gut. Achte auf NTP-Sync (Token-Ablauf).
- **HTTPS**: Redirect-URIs und Basis-URL **immer** https.
- **afterLogin**: Du übernimmst `afterLogin` bereits – super. Für Calls aus Nextcloud (External) kannst du in der External-App direkt `...?auto=1&afterLogin=/ziel` setzen.
---
Wenn du mir noch deine `providerConfig` (JSON/YAML) zeigst, kann ich dir die `attributeMap` exakt auf die OCS-Antwort biegen. Außerdem sag Bescheid, ob du die Auto-SSO-Logik lieber **im Plugin** (wie oben) oder **zentral im Front-Controller** haben willst – dann passe ich die Stellen genau auf deinen Codepfad an.