# KnpUOAuth2ClientBundle KnpUOAuth2ClientBundle is a Symfony bundle that integrates the `league/oauth2-client` library into Symfony applications, providing a streamlined way to implement OAuth2-based "social login" and API access flows. It supports over 50 built-in OAuth2 providers (Facebook, Google, GitHub, GitLab, Azure, Keycloak, and many more) and allows custom/generic providers, all wired into Symfony's service container automatically via YAML/PHP configuration. The bundle handles the complete authorization code grant flow — generating redirect URLs, storing CSRF state in the session, exchanging authorization codes for access tokens, and fetching user resource owner information. The bundle exposes typed client services (e.g., `knpu.oauth2.client.facebook`) per configured provider, accessible via the `ClientRegistry`. It integrates natively with Symfony Security through the modern `OAuth2Authenticator` base class (Symfony 5.2+) as well as the legacy Guard-based `SocialAuthenticator`. PKCE (Proof Key for Code Exchange) support is included for providers that require it. Helper traits cover common authentication patterns like saving error messages to session, redirecting to a pre-login URL, and gating users to a "finish registration" step. --- ## Bundle Installation and Configuration Install the bundle and configure one or more OAuth2 clients in `config/packages/knpu_oauth2_client.yaml`. Each entry under `clients` creates a service named `knpu.oauth2.client.`. ```bash composer require knpuniversity/oauth2-client-bundle # Then install the provider library you need, e.g.: composer require league/oauth2-github ``` ```yaml # config/packages/knpu_oauth2_client.yaml knpu_oauth2_client: # Optional: custom Guzzle HTTP client service # http_client: my_guzzle_client_service # Optional: default HTTP options # http_client_options: # timeout: 10 # proxy: null clients: # GitHub client — creates service: knpu.oauth2.client.github github_main: type: github client_id: '%env(OAUTH_GITHUB_CLIENT_ID)%' client_secret: '%env(OAUTH_GITHUB_CLIENT_SECRET)%' redirect_route: connect_github_check redirect_params: {} # use_state: true # default; set false for stateless flows # Google client — creates service: knpu.oauth2.client.google_main google_main: type: google client_id: '%env(OAUTH_GOOGLE_CLIENT_ID)%' client_secret: '%env(OAUTH_GOOGLE_CLIENT_SECRET)%' redirect_route: connect_google_check redirect_params: {} access_type: offline # optional — for refresh tokens hosted_domain: mycompany.com # optional — restrict to a G Suite domain # Facebook client facebook_main: type: facebook client_id: '%env(OAUTH_FACEBOOK_CLIENT_ID)%' client_secret: '%env(OAUTH_FACEBOOK_CLIENT_SECRET)%' redirect_route: connect_facebook_check redirect_params: {} graph_api_version: v2.12 # Keycloak with PKCE keycloak_pkce: type: keycloak_pkce client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%' client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%' redirect_route: connect_keycloak_check redirect_params: {} auth_server_url: 'https://keycloak.example.com/auth' realm: 'my-realm' # Generic / custom provider my_custom_oauth: type: generic client_id: '%env(OAUTH_CUSTOM_CLIENT_ID)%' client_secret: '%env(OAUTH_CUSTOM_CLIENT_SECRET)%' redirect_route: connect_custom_check redirect_params: {} provider_class: App\OAuth\MyCustomProvider # client_class: App\OAuth\MyCustomClient # optional override provider_options: base_url: 'https://oauth.example.com' ``` --- ## `ClientRegistry::getClient()` — Retrieve a Configured OAuth2 Client `ClientRegistry` is the central service for accessing any configured OAuth2 client by its key. It lazily instantiates client objects, making it safe to inject into authenticators that run on every request. ```php // src/Controller/ConnectController.php namespace App\Controller; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; class ConnectController extends AbstractController { public function __construct(private ClientRegistry $clientRegistry) {} #[Route('/connect/github', name: 'connect_github_start')] public function connectGithub(): Response { // Redirects user to GitHub for authorization return $this->clientRegistry ->getClient('github_main') // key from config ->redirect(['read:user', 'user:email']); // requested scopes } #[Route('/connect/google', name: 'connect_google_start')] public function connectGoogle(): Response { return $this->clientRegistry ->getClient('google_main') ->redirect(['openid', 'email', 'profile'], [ 'prompt' => 'select_account', // extra provider option ]); } // List all enabled client keys public function listProviders(): array { return $this->clientRegistry->getEnabledClientKeys(); // => ['github_main', 'google_main', 'facebook_main', ...] } } ``` --- ## `OAuth2Client::redirect()` — Start the OAuth2 Authorization Flow Generates an authorization URL and returns a `RedirectResponse` to send the user to the OAuth2 provider. Automatically stores the CSRF `state` parameter in the session unless `use_state: false` is configured or `setAsStateless()` was called. ```php // src/Controller/ConnectController.php use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; #[Route('/connect/facebook', name: 'connect_facebook_start')] public function connectFacebook(ClientRegistry $clientRegistry): Response { $client = $clientRegistry->getClient('facebook_main'); // Optionally mark as stateless (no session state check) // $client->setAsStateless(); // Redirect to Facebook; scopes are passed as first argument return $client->redirect( ['public_profile', 'email'], // Extra options become query params on the authorization URL: ['auth_type' => 'rerequest'] ); // => RedirectResponse to https://www.facebook.com/dialog/oauth?client_id=...&state=... } ``` --- ## `OAuth2Client::getAccessToken()` — Exchange Authorization Code for Token Called in the "check" controller or authenticator after the provider redirects back. Validates the `state` parameter from the session (CSRF protection), reads the `code` query parameter, and exchanges it for an `AccessToken`. ```php // src/Controller/ConnectController.php use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use KnpU\OAuth2ClientBundle\Exception\InvalidStateException; use KnpU\OAuth2ClientBundle\Exception\MissingAuthorizationCodeException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; #[Route('/connect/github/check', name: 'connect_github_check')] public function connectGithubCheck(Request $request, ClientRegistry $clientRegistry): mixed { $client = $clientRegistry->getClient('github_main'); try { /** @var AccessToken $accessToken */ $accessToken = $client->getAccessToken(); // $accessToken->getToken() => 'gho_abc123...' // $accessToken->getExpires() => 1700000000 (unix timestamp or null) // $accessToken->getRefreshToken() => 'ghr_xyz...' (if provided) // $accessToken->hasExpired() => false // Optionally pass extra options to the token request: // $accessToken = $client->getAccessToken(['resource' => 'https://api.example.com']); // Now fetch the user using the token $githubUser = $client->fetchUserFromToken($accessToken); return $this->redirectToRoute('app_home'); } catch (InvalidStateException $e) { // CSRF state mismatch return new Response('Invalid state: '.$e->getMessage(), 403); } catch (MissingAuthorizationCodeException $e) { // No "code" in the query string return new Response('No authorization code received.', 400); } catch (IdentityProviderException $e) { // Provider returned an error (e.g., user denied access) return new Response('Provider error: '.$e->getMessage(), 400); } } ``` --- ## `OAuth2Client::fetchUser()` / `fetchUserFromToken()` — Get Resource Owner Info `fetchUser()` is a shortcut that calls `getAccessToken()` then `fetchUserFromToken()` in one step. Use `fetchUserFromToken()` when you already have a token (e.g., inside an authenticator). The returned object is a `ResourceOwnerInterface` whose concrete class depends on the provider. ```php use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use League\OAuth2\Client\Token\AccessToken; // --- Shortcut: fetch token + user in one call --- $googleUser = $clientRegistry->getClient('google_main')->fetchUser(); // Methods depend on the provider library installed: echo $googleUser->getEmail(); // 'user@example.com' echo $googleUser->getName(); // 'Jane Doe' echo $googleUser->getId(); // '1234567890' // --- When you already have the AccessToken --- /** @var AccessToken $token */ $token = $clientRegistry->getClient('facebook_main')->getAccessToken(); $facebookUser = $clientRegistry->getClient('facebook_main')->fetchUserFromToken($token); echo $facebookUser->getFirstName(); // 'Jane' echo $facebookUser->getEmail(); // 'jane@example.com' echo $facebookUser->getId(); // '98765432' // --- Access the raw league/oauth2-client provider for advanced use --- $provider = $clientRegistry->getClient('facebook_main')->getOAuth2Provider(); // e.g. Facebook-specific long-lived token exchange: $longLivedToken = $provider->getLongLivedAccessToken($token); ``` --- ## `OAuth2Client::refreshAccessToken()` — Renew an Expired Token Exchanges a refresh token string for a new `AccessToken`. Requires the provider to support the `refresh_token` grant and that the token was originally obtained with the appropriate scope (e.g., `offline_access` or `offline`). ```php use League\OAuth2\Client\Token\AccessToken; $client = $clientRegistry->getClient('google_main'); // Load stored token from session or database /** @var AccessToken $storedToken */ $storedToken = $session->get('oauth_token'); if ($storedToken->hasExpired()) { $newToken = $client->refreshAccessToken( $storedToken->getRefreshToken(), // Some providers require extra options, e.g.: // ['scope' => 'openid email profile offline_access'] ); // Persist the new token for next time $session->set('oauth_token', $newToken); // Or if stored in DB: $user->setRefreshToken($newToken->getRefreshToken()); $entityManager->flush(); $storedToken = $newToken; } // Use the valid token $userData = $client->fetchUserFromToken($storedToken); ``` --- ## `OAuth2Authenticator` — Modern Symfony Authenticator (Symfony 5.2+) Extend `OAuth2Authenticator` to implement a Symfony security authenticator. The base class provides `fetchAccessToken()` (which maps OAuth exceptions to Symfony security exceptions) and the `FinishRegistrationBehavior`, `PreviousUrlHelper`, and `SaveAuthFailureMessage` traits. ```php // src/Security/GitHubAuthenticator.php namespace App\Security; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator; use League\OAuth2\Client\Provider\GithubResourceOwner; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; 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\Security\Http\EntryPoint\AuthenticationEntryPointInterface; class GitHubAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface { public function __construct( private ClientRegistry $clientRegistry, private EntityManagerInterface $em, private RouterInterface $router, ) {} // Only activate on the GitHub check route public function supports(Request $request): ?bool { return $request->attributes->get('_route') === 'connect_github_check'; } public function authenticate(Request $request): Passport { $client = $this->clientRegistry->getClient('github_main'); // fetchAccessToken() wraps getAccessToken() and converts OAuth exceptions // to Symfony security exceptions (NoAuthCodeAuthenticationException, etc.) $accessToken = $this->fetchAccessToken($client); return new SelfValidatingPassport( new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) { /** @var GithubResourceOwner $githubUser */ $githubUser = $client->fetchUserFromToken($accessToken); // 1) Already linked? $existing = $this->em->getRepository(User::class) ->findOneBy(['githubId' => $githubUser->getId()]); if ($existing) { return $existing; } // 2) Match by email? $user = $this->em->getRepository(User::class) ->findOneBy(['email' => $githubUser->getEmail()]); if ($user) { $user->setGithubId($githubUser->getId()); $this->em->flush(); return $user; } // 3) Register new user $user = new User(); $user->setEmail($githubUser->getEmail()); $user->setGithubId($githubUser->getId()); $this->em->persist($user); $this->em->flush(); return $user; }) ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // Redirect to the page they were trying to access, or home $targetUrl = $this->getPreviousUrl($request, $firewallName) ?: $this->router->generate('app_home'); return new RedirectResponse($targetUrl); } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { // Save error to session so it can be shown on the login page $this->saveAuthenticationErrorToSession($request, $exception); return new RedirectResponse($this->router->generate('app_login')); } // Called when authentication is required but not yet attempted public function start(Request $request, ?AuthenticationException $authException = null): Response { return new RedirectResponse($this->router->generate('app_login')); } } ``` ```yaml # config/packages/security.yaml security: firewalls: main: custom_authenticators: - App\Security\GitHubAuthenticator # For Symfony 6.4 and below, also enable: # enable_authenticator_manager: true ``` --- ## `OAuth2Authenticator::fetchAccessToken()` — Safe Token Fetch in Authenticator Context A protected helper method on `OAuth2Authenticator` that wraps `OAuth2ClientInterface::getAccessToken()` and converts OAuth-specific exceptions into Symfony security exceptions suitable for the authenticator framework. ```php // Inside any class extending OAuth2Authenticator: public function authenticate(Request $request): Passport { $client = $this->clientRegistry->getClient('google_main'); // Converts exceptions: // MissingAuthorizationCodeException => NoAuthCodeAuthenticationException // IdentityProviderException => IdentityProviderAuthenticationException // InvalidStateException => InvalidStateAuthenticationException $accessToken = $this->fetchAccessToken($client); // Pass extra options to the underlying token request if needed: // $accessToken = $this->fetchAccessToken($client, ['resource' => 'https://api.example.com']); return new SelfValidatingPassport( new UserBadge($accessToken->getToken(), fn() => $this->loadUser($accessToken, $client)) ); } ``` --- ## `OAuth2PKCEClient` — PKCE Extension Support `OAuth2PKCEClient` extends `OAuth2Client` to support the PKCE (Proof Key for Code Exchange) extension. It automatically generates a `code_verifier`, stores it in the session, and appends a `code_challenge` (SHA-256, Base64url-encoded) to the authorization URL. The verifier is sent automatically during token exchange. ```yaml # config/packages/knpu_oauth2_client.yaml knpu_oauth2_client: clients: keycloak_pkce: type: keycloak_pkce client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%' client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%' redirect_route: connect_keycloak_check redirect_params: {} auth_server_url: 'https://keycloak.example.com/auth' realm: 'myrealm' ``` ```php // src/Controller/ConnectController.php use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; #[Route('/connect/keycloak', name: 'connect_keycloak_start')] public function connectKeycloak(ClientRegistry $clientRegistry): Response { // redirect() automatically adds code_challenge and code_challenge_method=S256 // and stores the code_verifier in the session return $clientRegistry->getClient('keycloak_pkce')->redirect(['openid', 'profile', 'email']); } #[Route('/connect/keycloak/check', name: 'connect_keycloak_check')] public function connectKeycloakCheck(ClientRegistry $clientRegistry): Response { $client = $clientRegistry->getClient('keycloak_pkce'); // getAccessToken() automatically reads the code_verifier from session // and appends it to the token exchange request, then clears it from session $accessToken = $client->getAccessToken(); $user = $client->fetchUserFromToken($accessToken); return $this->redirectToRoute('app_home'); } ``` --- ## `OAuthUserProvider` / `OAuthUser` — Simple User Provider for Stateless Auth `OAuthUserProvider` is a built-in Symfony `UserProviderInterface` implementation for use when you don't need to persist users in a database. It creates `OAuthUser` instances with `ROLE_USER` and `ROLE_OAUTH_USER` roles. Register it in `security.yaml` to avoid writing a custom user provider. ```yaml # config/packages/security.yaml security: providers: oauth: id: knpu.oauth2.user_provider firewalls: main: provider: oauth custom_authenticators: - App\Security\MyAuthenticator ``` ```php // In your authenticator's authenticate() method, use the provider // to load users by their OAuth identifier (e.g., GitHub numeric ID): public function authenticate(Request $request): Passport { $client = $this->clientRegistry->getClient('github_main'); $accessToken = $this->fetchAccessToken($client); return new SelfValidatingPassport( new UserBadge( $accessToken->getToken(), function () use ($accessToken, $client) { $githubUser = $client->fetchUserFromToken($accessToken); // OAuthUserProvider::loadUserByIdentifier() returns an OAuthUser // with roles: ['ROLE_USER', 'ROLE_OAUTH_USER'] return $this->userProvider->loadUserByIdentifier( (string) $githubUser->getId() ); // Equivalent: new OAuthUser($githubUser->getId(), ['ROLE_USER', 'ROLE_OAUTH_USER']) } ) ); } ``` --- ## `FinishRegistrationBehavior` Trait — Gate Users to a Registration Step A trait available on `OAuth2Authenticator` for workflows where a newly OAuth-authenticated user must complete a registration form (e.g., choose a username) before getting a session. Throw a `FinishRegistrationException` from the `UserBadge` callback, save user info to session, then redirect. ```php // src/Security/GitHubAuthenticator.php use KnpU\OAuth2ClientBundle\Security\Exception\FinishRegistrationException; use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator; class GitHubAuthenticator extends OAuth2Authenticator { public function authenticate(Request $request): Passport { $client = $this->clientRegistry->getClient('github_main'); $accessToken = $this->fetchAccessToken($client); return new SelfValidatingPassport( new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) { $githubUser = $client->fetchUserFromToken($accessToken); $user = $this->em->getRepository(User::class) ->findOneBy(['githubId' => $githubUser->getId()]); if (!$user) { // New user — throw to redirect them to a registration form // The $githubUser object will be stored in the session throw new FinishRegistrationException($githubUser); } return $user; }) ); } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { if ($exception instanceof FinishRegistrationException) { // Store the resource owner data in session under // 'guard.finish_registration.user_information' $this->saveUserInfoToSession($request, $exception); return new RedirectResponse($this->router->generate('app_register_finish')); } return new RedirectResponse($this->router->generate('app_login')); } } // src/Controller/RegistrationController.php — retrieve saved info #[Route('/register/finish', name: 'app_register_finish')] public function finishRegistration(Request $request, GitHubAuthenticator $auth): Response { // Retrieve the ResourceOwnerInterface object saved during auth failure $githubUser = $auth->getUserInfoFromSession($request); // $githubUser->getEmail(), $githubUser->getName(), etc. // Render registration form pre-populated with OAuth data return $this->render('registration/finish.html.twig', ['githubUser' => $githubUser]); } ``` --- ## `PreviousUrlHelper` Trait — Redirect Back After Login A trait on `OAuth2Authenticator` to redirect users back to the URL they were trying to access before being forced to log in via OAuth. Reads from Symfony's standard `_security..target_path` session key. ```php // Inside your authenticator's onAuthenticationSuccess(): public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // Returns the URL the user was trying to reach, or empty string if none $previousUrl = $this->getPreviousUrl($request, $firewallName); if ($previousUrl) { return new RedirectResponse($previousUrl); } // Fall back to a default route return new RedirectResponse($this->router->generate('app_dashboard')); } ``` --- ## `SaveAuthFailureMessage` Trait — Persist Authentication Errors to Session A trait on `OAuth2Authenticator` that saves an `AuthenticationException` to the session under the standard Symfony `AUTHENTICATION_ERROR` key, making it retrievable with `{{ error }}` in Twig login templates. ```php // Inside your authenticator's onAuthenticationFailure(): public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { // Saves exception to session under SecurityRequestAttributes::AUTHENTICATION_ERROR $this->saveAuthenticationErrorToSession($request, $exception); return new RedirectResponse($this->router->generate('app_login')); } ``` ```twig {# templates/security/login.html.twig #} {% if error %}
{{ error.messageKey | trans(error.messageData, 'security') }}
{% endif %} ``` --- ## Generic Provider Configuration — Custom OAuth2 Server Use `type: generic` in the bundle config to connect to any OAuth2 server not in the built-in list, by pointing to a custom class that extends `league/oauth2-client`'s `AbstractProvider`. ```php // src/OAuth/MyCustomProvider.php namespace App\OAuth; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Token\AccessToken; use Psr\Http\Message\ResponseInterface; class MyCustomProvider extends AbstractProvider { public function __construct(private string $baseUrl, array $options = [], array $collaborators = []) { parent::__construct($options, $collaborators); } public function getBaseAuthorizationUrl(): string { return $this->baseUrl . '/oauth/authorize'; } public function getBaseAccessTokenUrl(array $params): string { return $this->baseUrl . '/oauth/token'; } public function getResourceOwnerDetailsUrl(AccessToken $token): string { return $this->baseUrl . '/api/me'; } protected function getDefaultScopes(): array { return ['profile', 'email']; } protected function checkResponse(ResponseInterface $response, $data): void { if (!empty($data['error'])) { throw new \Exception($data['error_description'] ?? $data['error']); } } protected function createResourceOwner(array $response, AccessToken $token): \League\OAuth2\Client\Provider\ResourceOwnerInterface { return new \League\OAuth2\Client\Provider\GenericResourceOwner($response, 'id'); } } ``` ```yaml # config/packages/knpu_oauth2_client.yaml knpu_oauth2_client: clients: my_custom_oauth: type: generic client_id: '%env(OAUTH_CUSTOM_CLIENT_ID)%' client_secret: '%env(OAUTH_CUSTOM_CLIENT_SECRET)%' redirect_route: connect_custom_check redirect_params: {} provider_class: App\OAuth\MyCustomProvider provider_options: base_url: 'https://oauth.example.com' ``` --- ## Extending / Decorating a Client Class Replace the auto-registered client service with a custom decorator (e.g., to add caching) by using Symfony's service decoration. The decorator must implement `OAuth2ClientInterface`. ```php // src/Client/CachingGithubClient.php namespace App\Client; use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface; use KnpU\OAuth2ClientBundle\Client\Provider\GithubClient; use League\OAuth2\Client\Token\AccessToken; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; class CachingGithubClient implements OAuth2ClientInterface { public function __construct( private GithubClient $inner, private CacheInterface $cache, ) {} public function fetchUserFromToken(AccessToken $accessToken) { return $this->cache->get( 'github_user_' . md5($accessToken->getToken()), function (ItemInterface $item) use ($accessToken) { $item->expiresAfter(300); // Cache for 5 minutes return $this->inner->fetchUserFromToken($accessToken); } ); } // Delegate all other methods to the inner client public function setAsStateless(): void { $this->inner->setAsStateless(); } public function redirect(array $scopes = [], array $options = []) { return $this->inner->redirect($scopes, $options); } public function getAccessToken(array $options = []) { return $this->inner->getAccessToken($options); } public function fetchUser() { return $this->inner->fetchUser(); } public function getOAuth2Provider() { return $this->inner->getOAuth2Provider(); } } ``` ```yaml # config/services.yaml services: App\Client\CachingGithubClient: decorates: knpu.oauth2.client.github_main arguments: $cache: '@cache.app' ``` --- KnpUOAuth2ClientBundle is the standard solution for adding OAuth2 / social login to Symfony applications. Its most common use cases are: social login ("Sign in with GitHub/Google/Facebook"), service integrations that need OAuth2 access tokens for API calls (e.g., reading a user's Google Calendar or posting to their Slack), and enterprise SSO flows using providers like Keycloak, Azure AD, or Okta — including PKCE-secured flows for public clients. The bundle handles the entire authorization code grant lifecycle while remaining unopinionated about how users are persisted. Integration with Symfony Security is the primary deployment pattern: configure one client per provider in YAML, write a single authenticator class extending `OAuth2Authenticator`, and register it under `custom_authenticators` in `security.yaml`. For simple stateless use cases (e.g., just obtaining an API token without logging users into Symfony's security system), the controller pattern — injecting `ClientRegistry`, calling `redirect()` on the start route and `getAccessToken()` / `fetchUser()` on the check route — is sufficient without any security configuration. The `ClientRegistry` service is the single injection point for all provider clients, keeping controller and authenticator constructors lean.