OAuth2 und OpenID Connect in Symfony integrieren: Praxisleitfaden
AI generated
SF
{ }
Symfony · OAuth2 · OpenID Connect · SSO
OAuth2 und OpenID Connect
in Symfony integrieren, Schritt für Schritt

Single Sign-On über einen externen Identity Provider verlangt mehr als das Weiterleiten eines Login-Buttons. Dieser Leitfaden zeigt, wie OAuth2 und OpenID Connect in Symfony sauber integriert werden, vom Authorization Code Flow über die ID-Token-Validierung bis zum Rollen-Mapping in die eigene Benutzerverwaltung.

19 Min. Lesezeit Authorization Code Flow · PKCE · ID-Token · Rollen-Mapping Symfony 6.4 · 7.x · PHP 8.2+

1. OAuth2 und OpenID Connect: zwei Protokolle, ein Missverständnis

OAuth2 ist ein Autorisierungsprotokoll, kein Authentifizierungsprotokoll. Es beantwortet die Frage, welche Ressourcen ein Client im Namen eines Benutzers abrufen darf, nicht wer dieser Benutzer eigentlich ist. Genau dieses Missverständnis führt in vielen Projekten dazu, dass ein Access Token fälschlich als Identitätsnachweis behandelt wird. OpenID Connect setzt exakt hier an: es erweitert OAuth2 um ein signiertes ID-Token, das kryptografisch verifizierbare Aussagen über die Identität des Benutzers trifft.

Für eine korrekte OAuth2-Integration in Symfony bedeutet das: der Login-Flow selbst nutzt OAuth2-Mechanik, aber die eigentliche Authentifizierung stützt sich auf das OIDC-ID-Token, nicht auf das Access Token. Diese Unterscheidung ist keine Formalität, sie entscheidet darüber, ob eine Anwendung tatsächlich sicher ist oder nur so aussieht. Der komplette Leitfaden folgt dieser Trennung: OAuth2 für den Autorisierungsfluss, OIDC für die Identität.

2. Der Authorization Code Flow mit PKCE im Detail

Der Authorization Code Flow ist für serverseitige Symfony-Anwendungen der einzig richtige OAuth2-Flow. Der Browser wird zum Identity Provider umgeleitet, der Benutzer authentifiziert sich dort, und der Provider leitet mit einem einmaligen Autorisierungscode zurück zur Anwendung. Erst der Symfony-Server tauscht diesen Code serverseitig gegen Access Token und ID-Token, wobei das Client-Secret niemals den Browser erreicht.

PKCE, Proof Key for Code Exchange, ergänzt diesen Flow um einen zusätzlichen Schutz gegen Code-Interception-Angriffe: die Anwendung erzeugt einen zufälligen code_verifier, sendet dessen Hash als code_challenge bei der initialen Anfrage, und muss beim Token-Austausch den ursprünglichen Verifier vorlegen. Auch wenn PKCE ursprünglich für Public Clients ohne Secret gedacht war, empfiehlt der aktuelle OAuth2-Security-BCP PKCE inzwischen für jeden OAuth2-Client, unabhängig vom Client-Typ.

3. knpuniversity/oauth2-client-bundle einrichten

Für die technische Umsetzung in Symfony hat sich knpuniversity/oauth2-client-bundle als Standard etabliert, weil es die Low-Level-Details des Token-Austauschs kapselt und gleichzeitig genug Kontrolle für eigene OpenID Connect-Logik lässt. Die Konfiguration definiert Client-ID, Secret und die drei Endpunkte des Identity Providers: Authorization-Endpoint, Token-Endpoint und die Discovery-URL für die restlichen OIDC-Metadaten.

Wichtig bei der Einrichtung: die Redirect-URI muss exakt mit der beim Identity Provider registrierten URI übereinstimmen, inklusive Protokoll, Port und eventuell vorhandenem trailing Slash. Eine Abweichung führt zu einer kryptischen Fehlermeldung des Providers, die selten direkt auf die eigentliche Ursache hinweist. Für lokale Entwicklung empfiehlt sich ein separater OAuth2-Client-Eintrag mit eigener Redirect-URI, statt Produktions- und Entwicklungsumgebung dieselbe Konfiguration teilen zu lassen.


# config/packages/knpu_oauth2_client.yaml
knpu_oauth2_client:
  clients:
    keycloak_client:
      type: generic
      # These three values come from your identity provider's admin console
      client_id: '%env(OIDC_CLIENT_ID)%'
      client_secret: '%env(OIDC_CLIENT_SECRET)%'
      redirect_route: connect_oidc_check
      redirect_params: {}
      # Discovery endpoint resolves authorization/token/jwks URLs automatically
      urlAuthorize: '%env(OIDC_AUTH_ENDPOINT)%'
      urlAccessToken: '%env(OIDC_TOKEN_ENDPOINT)%'
      urlResourceOwnerDetails: '%env(OIDC_USERINFO_ENDPOINT)%'
      scopes: ['openid', 'profile', 'email']

4. Eigener OAuth2-Authenticator für den Callback

Der Callback nach erfolgreichem Login beim Identity Provider landet in einem eigenen Authenticator, der auf OAuth2Authenticator aus dem Bundle aufbaut. Dieser tauscht den Autorisierungscode gegen die Tokens, ruft bei Bedarf den Userinfo-Endpoint auf und lädt oder erstellt den lokalen Benutzer anhand einer stabilen Kennung, meist dem sub-Claim aus dem ID-Token, niemals der E-Mail-Adresse, die sich beim Provider ändern kann.

Für neue Benutzer, die sich erstmalig per OAuth2 anmelden, entscheidet die Anwendung, ob automatisch ein lokaler Account angelegt wird, Just-in-Time-Provisioning genannt, oder ob eine explizite Einladung vorausgesetzt wird. Just-in-Time-Provisioning ist praktisch für interne Tools mit vertrauenswürdigem Identity Provider, aber riskant für öffentliche Anwendungen, bei denen jeder beliebige Google- oder Microsoft-Account sonst automatisch Zugriff erhielte.


<?php

declare(strict_types=1);

namespace App\Security;

use App\Repository\UserRepository;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use League\OAuth2\Client\Provider\GenericProvider;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Routing\RouterInterface;

final class OidcAuthenticator extends OAuth2Authenticator
{
    public function __construct(
        private readonly ClientRegistry $clientRegistry,
        private readonly UserRepository $users,
        private readonly RouterInterface $router,
    ) {
    }

    public function supports(Request $request): ?bool
    {
        return $request->attributes->get('_route') === 'connect_oidc_check';
    }

    public function authenticate(Request $request): Passport
    {
        /** @var GenericProvider $client */
        $client = $this->clientRegistry->getClient('keycloak_client')->getOAuth2Provider();
        $accessToken = $this->fetchAccessToken($this->clientRegistry->getClient('keycloak_client'));

        // The subject claim is the stable, provider-issued identifier — never the email
        $idTokenClaims = $this->decodeAndVerifyIdToken((string) $accessToken->getValues()['id_token']);
        $subject = $idTokenClaims['sub'];

        return new SelfValidatingPassport(new UserBadge($subject, function (string $sub) use ($idTokenClaims) {
            $user = $this->users->findByOidcSubject($sub);
            if (null === $user) {
                throw new CustomUserMessageAuthenticationException('No local account provisioned for this identity.');
            }
            return $user;
        }));
    }

    public function onAuthenticationSuccess($request, $token, string $firewallName): ?RedirectResponse
    {
        return new RedirectResponse($this->router->generate('app_dashboard'));
    }
}

5. ID-Token validieren statt blind vertrauen

Das ID-Token ist ein signiertes JWT, und diese Signatur muss geprüft werden, bevor irgendein Claim daraus vertraut wird. Die Verifikation läuft über den öffentlichen Schlüssel des Identity Providers, veröffentlicht am JWKS-Endpoint aus den OIDC-Discovery-Metadaten. Eine Bibliothek wie web-token/jwt-framework übernimmt Signaturprüfung, Schlüsselrotation und die Validierung der Standard-Claims wie exp, iss und aud zuverlässig, ein manueller JWT-Parser ohne vollständige Signaturprüfung ist für OpenID Connect nicht ausreichend sicher.

Drei Claims verdienen besondere Aufmerksamkeit bei jeder OpenID Connect-Integration: iss muss exakt der erwarteten Issuer-URL entsprechen, sonst könnte ein Token von einem anderen, möglicherweise kompromittierten Provider stammen. aud muss die eigene Client-ID enthalten, sonst war das Token für eine andere Anwendung ausgestellt. exp muss in der Zukunft liegen, mit einer kleinen Toleranz von wenigen Sekunden für Uhrzeit-Abweichungen zwischen Server und Provider.


<?php

declare(strict_types=1);

namespace App\Security\Oidc;

use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Checker\ExpirationTimeChecker;
use Jose\Component\Checker\IssuedAtChecker;
use Jose\Component\Signature\JWSVerifier;

final class IdTokenValidator
{
    public function __construct(
        private readonly JWSVerifier $jwsVerifier,
        private readonly ClaimCheckerManager $claimCheckers,
        private readonly string $expectedIssuer,
        private readonly string $clientId,
    ) {
    }

    public function validate(string $idToken): array
    {
        // 1. Signature must verify against the provider's published JWKS
        $jws = $this->deserializeAndVerifySignature($idToken);
        $claims = json_decode($jws->getPayload(), true, flags: JSON_THROW_ON_ERROR);

        // 2. iss, aud and exp are checked explicitly — never trust unverified claims
        if (($claims['iss'] ?? null) !== $this->expectedIssuer) {
            throw new \RuntimeException('Unexpected issuer.');
        }
        if (!in_array($this->clientId, (array) ($claims['aud'] ?? []), true)) {
            throw new \RuntimeException('Token was not issued for this client.');
        }

        $this->claimCheckers->check($claims, [new ExpirationTimeChecker(), new IssuedAtChecker()]);

        return $claims;
    }
}

6. Rollen-Mapping vom Identity Provider in Symfony

Ein Identity Provider liefert häufig eigene Gruppen- oder Rollen-Claims, etwa groups bei Keycloak oder roles bei Auth0, die nicht eins zu eins den Symfony-Rollen entsprechen müssen. Ein zentrales Mapping übersetzt diese externen Claims in interne Rollen wie ROLE_ADMIN oder ROLE_EDITOR, idealerweise über eine konfigurierbare Zuordnungstabelle statt hartkodierter if-Verzweigungen, damit Änderungen am Provider-seitigen Rollenschema nicht sofort Code-Änderungen erfordern.

Ein subtiler Fehler bei OAuth2/OIDC-Rollen-Mapping: Rollen werden nur beim ersten Login übernommen und danach nie mehr aktualisiert, obwohl sich die Gruppenzugehörigkeit beim Provider längst geändert hat. Ein robustes Muster synchronisiert Rollen bei jedem Login neu aus den aktuellen ID-Token-Claims, statt sich auf einen einmaligen Import zu verlassen. So bleibt ein entfernter Mitarbeiter nicht versehentlich mit veralteten Admin-Rechten in der Anwendung.


# config/packages/oidc_role_mapping.yaml
parameters:
  # Configurable mapping instead of hardcoded if-branches —
  # updating the provider's group scheme never requires a code change
  oidc.role_mapping:
    'keycloak-admins': 'ROLE_ADMIN'
    'keycloak-editors': 'ROLE_EDITOR'
    'keycloak-support': 'ROLE_SUPPORT'
    # Any group not listed here maps to no additional role

7. Session, Logout und Token-Refresh

Nach erfolgreicher OpenID Connect-Authentifizierung führt Symfony eine normale, klassische Session, unabhängig vom externen Provider. Das bedeutet: ein Logout in der Symfony-Anwendung meldet den Benutzer standardmäßig nicht auch beim Identity Provider ab. Für echtes Single Sign-Out ist ein zusätzlicher Redirect zum end_session_endpoint des Providers nötig, den OIDC-Discovery-Metadaten in der Regel mitliefern.

Der Access Token selbst wird nach dem initialen Login meist nicht mehr benötigt, außer die Anwendung ruft aktiv APIs des Identity Providers auf. Falls doch, sorgt ein Refresh Token dafür, dass ein abgelaufener Access Token ohne erneuten Login-Redirect erneuert werden kann. Dieser Refresh Token gehört, genau wie bei der reinen API Token Authentifizierung, ausschließlich serverseitig gespeichert, niemals im Browser oder in einem clientseitigen Cookie.


<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Attribute\Route;

final class SingleSignOutController extends AbstractController
{
    #[Route('/logout/complete', name: 'app_single_sign_out')]
    public function __invoke(): RedirectResponse
    {
        // Local Symfony session is already cleared by the firewall's logout handler.
        // This redirect additionally ends the session at the identity provider.
        $endSessionEndpoint = $this->getParameter('oidc.end_session_endpoint');

        return new RedirectResponse($endSessionEndpoint . '?post_logout_redirect_uri=' . urlencode(
            $this->generateUrl('app_home', [], 0)
        ));
    }
}

8. Typische Integrationsfehler und ihre Ursachen

Der häufigste Fehler bei OAuth2-Integrationen: das Access Token wird direkt zur Authentifizierung genutzt, ohne das ID-Token überhaupt anzufordern oder zu validieren. Ein Access Token hat kein garantiertes, standardisiertes Format, seine Gültigkeit lässt sich oft nur durch einen zusätzlichen Request an den Provider prüfen, was Latenz und eine zusätzliche Fehlerquelle bedeutet. Das ID-Token dagegen ist lokal, offline und kryptografisch verifizierbar, genau dafür wurde es entworfen.

Ein zweiter häufiger Fehler: der state-Parameter des Authorization Code Flows wird nicht geprüft, was die Anwendung anfällig für Cross-Site-Request-Forgery im Login-Flow macht. Das knpuniversity/oauth2-client-bundle handhabt state automatisch korrekt, ein manuell gebauter Flow ohne Bundle muss diesen Schritt explizit selbst implementieren, sonst kann ein Angreifer einen fremden Autorisierungscode in die Session des Opfers einschleusen.

9. OAuth2/OIDC im Vergleich zu anderen Login-Wegen

Nicht jedes Projekt profitiert von einer vollständigen OpenID Connect-Integration. Die folgende Übersicht ordnet die Login-Strategien nach Aufwand und Eignung ein, abhängig davon, ob bereits ein zentraler Identity Provider im Unternehmen existiert.

Login-Weg Eignung Aufwand Wann sinnvoll
Klassischer Formular-Login Einzelanwendung Gering Kein zentraler Identity Provider vorhanden
OAuth2 + eigenes ID-Token Unsicher Mittel Nie, kein Ersatz für OIDC
OAuth2 + OpenID Connect Unternehmens-SSO Mittel bis hoch Zentraler Provider vorhanden, mehrere Anwendungen
Social Login ohne eigenes Konto Consumer-Apps Gering bis mittel Öffentliche Registrierung erwünscht

Sobald ein Unternehmen mehr als eine interne Anwendung betreibt, zahlt sich der Mehraufwand einer sauberen OAuth2-plus-OIDC-Integration schnell aus: Benutzer verwalten Zugänge nur beim zentralen Provider, Passwort-Richtlinien und Mehrfaktor-Anforderungen gelten automatisch für alle angebundenen Anwendungen, und ein Mitarbeiterwechsel erfordert nur eine einzige Deaktivierung statt vieler einzelner Accounts.

Mironsoft

Single Sign-On, Identity-Integration und Symfony-Backend

Single Sign-On sauber an Ihren Identity Provider anbinden?

Wir integrieren OAuth2 und OpenID Connect in Symfony, inklusive korrekter ID-Token-Validierung, Rollen-Mapping aus Ihrem bestehenden Provider und funktionierendem Single Sign-Out.

SSO-Integration

Keycloak, Auth0, Azure AD oder eigenen Provider anbinden

Security-Review

Bestehende OAuth2-Flows auf Token-Verwechslung und CSRF prüfen

Rollen-Mapping

Konfigurierbares Mapping von Provider-Claims zu Symfony-Rollen

10. Zusammenfassung

OAuth2 regelt Autorisierung, OpenID Connect ergänzt Authentifizierung über ein signiertes ID-Token, und diese Unterscheidung entscheidet über die Sicherheit der gesamten Integration. Der Authorization Code Flow mit PKCE ist für Symfony-Anwendungen der richtige Flow, das Bundle knpuniversity/oauth2-client-bundle übernimmt die Low-Level-Mechanik, ein eigener Authenticator verarbeitet den Callback und lädt den lokalen Benutzer anhand des stabilen sub-Claims.

Die ID-Token-Signatur muss immer verifiziert werden, iss, aud und exp sind dabei die drei kritischen Claims. Rollen sollten bei jedem Login neu aus den Provider-Claims synchronisiert werden, nicht nur beim ersten Login. Wer mehr als eine Anwendung im Unternehmen betreibt, profitiert vom zentralisierten Zugriffsmanagement, das eine saubere OpenID Connect-Integration ermöglicht.

OAuth2 und OpenID Connect in Symfony — Das Wichtigste auf einen Blick

OAuth2 vs. OIDC

OAuth2 regelt Autorisierung, OIDC ergänzt ein signiertes ID-Token für die tatsächliche Identitätsprüfung.

Authorization Code Flow + PKCE

Der einzig richtige Flow für serverseitige Symfony-Apps, PKCE schützt zusätzlich gegen Code-Interception.

ID-Token immer validieren

Signatur gegen JWKS prüfen, iss, aud und exp verifizieren, niemals ungeprüften Claims vertrauen.

Rollen bei jedem Login synchronisieren

Kein einmaliger Import, sondern kontinuierliche Synchronisation aus den aktuellen Provider-Claims.

11. FAQ: OAuth2 und OpenID Connect in Symfony

1OAuth2 allein für Authentifizierung?
Nein, OAuth2 regelt nur Autorisierung. Für Identitätsprüfung ist OpenID Connect mit dem signierten ID-Token nötig.
2Welcher Flow für Symfony?
Authorization Code Flow mit PKCE, Client-Secret erreicht den Browser nie, Tausch findet serverseitig statt.
3sub statt E-Mail zur Identifikation?
E-Mail kann sich ändern, sub ist die stabile Kennung und sollte für die Kontoverknüpfung genutzt werden.
4ID-Token selbst validieren?
Signaturprüfung ist Pflicht, über eine etablierte JWT-Bibliothek, nicht über einen manuellen Parser ohne echte Prüfung.
5Logout und Provider-Session?
Normaler Logout beendet nur die lokale Session. Single Sign-Out braucht Redirect zum end_session_endpoint.
6Wie oft Rollen synchronisieren?
Bei jedem Login, sonst behält ein Benutzer veraltete Rollen nach Gruppenänderungen beim Provider.
7Was ist PKCE?
Zusätzlicher kryptografischer Nachweis gegen Code-Interception, inzwischen für alle Client-Typen empfohlen.
8Just-in-Time-Provisioning riskant?
Praktisch bei internen Tools, riskant bei öffentlichen Apps, da jeder externe Account sonst automatisch Zugriff erhält.
9Warum exakte Redirect-URI?
Der Provider vergleicht exakt inklusive Protokoll, Port und trailing Slash, Abweichungen führen zur Ablehnung.
10Access Token speichern?
Nur bei aktiven API-Aufrufen an den Provider nötig, dann ausschließlich serverseitig speichern.