KnpUOAuth2ClientBundle

repository·main·Indexed 21 days ago

https://github.com/knpuniversity/oauth2-client-bundle

A Symfony bundle that facilitates integration with OAuth2 providers like Facebook and GitHub. Acting as a wrapper for the league/oauth2-client library, it provides low-level control for social login, account connection, and API key retrieval. It supports Symfony Custom Authenticators for versions 5.2+ and Guard Authenticators for legacy applications.

Tokens
18.6K
Snippets
41
Records
44
Agent score
73%

What's inside KnpUOAuth2ClientBundle

  1. Overview of KnpUOAuth2ClientBundle capabilities

    main

    KnpUOAuth2ClientBundle provides an easy way to integrate with OAuth2 servers (such as Facebook or GitHub) within a Symfony application. It is designed for:

    • Social Authentication/Login: Implementing 'Login with [Provider]' features.
    • Account Connection: Implementing 'Connect with Facebook' style functionality.
    • API Access: Fetching access keys via OAuth2 to interact with external APIs.
    • Security Integration: Performing OAuth2 authentication using Symfony Custom Authenticator (or Guard Authenticator for legacy applications).

    This bundle acts as a wrapper and integration layer for the league/oauth2-client library.

  2. Choosing between KnpUOAuth2ClientBundle and HWIOAuthBundle

    main

    When deciding between knpuniversity/oauth2-client-bundle and hwi/oauth-bundle, consider your requirements for abstraction versus control:

    • Use hwi/oauth-bundle if you want more features out-of-the-box, specifically including social authentication and registration ('connect') workflows, and prefer a more automated setup.
    • Use knpuniversity/oauth2-client-bundle if you prefer more low-level control over the OAuth2 process and are willing to perform more manual setup to achieve it.
  3. Decorate OAuth2 client classes

    main

    If you need to add custom logic (like caching) to an existing client, you can decorate the client service.

    1. Create a new class that implements KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface.
    2. Inject the original client into your constructor.
    3. Override the public methods of the interface, delegating to the original client while adding your custom behavior.
    4. Register the decoration in config/services.yaml using the decorates key pointing to the original bundle service ID (e.g., knpu.oauth2.client.azure).
    namespace App\Client;
    
    use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
    use KnpU\OAuth2ClientBundle\Client\Provider\AzureClient;
    use Symfony\Component\Cache\Adapter\AdapterInterface;
    
    class CacheableAzureClient implements OAuth2ClientInterface
    {
        private $client;
        private $cache;
    
        public function __construct(AzureClient $client, AdapterInterface $cache)
        {
            // ...
        }
    
        // override all public functions and call the method on the internal $this->client object
        // but add caching wherever you need it
    }
    # config/services.yaml
    services:
        App\Client\CacheableAzureClient:
            decorates: knpu.oauth2.client.azure
  4. Authenticate users with the new Symfony Authenticator

    main

    For Symfony 5.2 and higher, use the OAuth2Authenticator class to handle the login process. This class allows you to intercept the OAuth callback, fetch the access token, and resolve the user from the provider (e.g., Facebook).

    Implementation Steps:

    1. Create an Authenticator: Extend KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator and implement AuthenticationEntryPointInterface.
    2. Implement supports(): Return true only if the current request matches your OAuth callback route.
    3. Implement authenticate():
      • Use ClientRegistry::getClient() to get your configured client.
      • Use $this->fetchAccessToken($client) to retrieve the token.
      • Return a SelfValidatingPassport containing a UserBadge. Inside the UserBadge callback, use $client->fetchUserFromToken($accessToken) to get provider-specific user data and resolve your local User entity.
    4. Implement onAuthenticationSuccess(): Redirect the user to a target page (e.g., your homepage) or return null to let the request continue to the controller.
    5. Implement start(): Define where users are redirected when authentication is required but not yet sent (e.g., a provider selection page).

    Best Practice: Use ClientRegistry

    Inject ClientRegistry into your authenticator instead of individual client classes (like FacebookClient). ClientRegistry lazily creates client objects, which prevents circular reference issues and improves performance since authenticators are instantiated on every request.

    namespace App\Security;
    
    use App\Entity\User;
    use Doctrine\ORM\EntityManagerInterface;
    use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
    use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
    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 MyFacebookAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
    {
        private $clientRegistry;
        private $entityManager;
        private $router;
    
        public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router)
        {
            $this->clientRegistry = $clientRegistry;
            $this->entityManager = $entityManager;
            $this->router = $router;
        }
    
        public function supports(Request $request): ?bool
        {
            return $request->attributes->get('_route') === 'connect_facebook_check';
        }
    
        public function authenticate(Request $request): Passport
        {
            $client = $this->clientRegistry->getClient('facebook_main');
            $accessToken = $this->fetchAccessToken($client);
    
            return new SelfValidatingPassport(
                new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
                    $facebookUser = $client->fetchUserFromToken($accessToken);
                    $email = $facebookUser->getEmail();
    
                    $existingUser = $this->entityManager->getRepository(User::class)->findOneBy(['facebookId' => $facebookUser->getId()]);
                    if ($existingUser) {
                        return $existingUser;
                    }
    
                    $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $email]);
                    $user->setFacebookId($facebookUser->getId());
                    $this->entityManager->persist($user);
                    $this->entityManager->flush();
    
                    return $user;
                })
            );
        }
    
        public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
        {
            $targetUrl = $this->router->generate('app_homepage');
            return new RedirectResponse($targetUrl);
        }
    
        public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
        {
            $message = strtr($exception->getMessageKey(), $exception->getMessageData());
            return new Response($message, Response::HTTP_FORBIDDEN);
        }
    
        public function start(Request $request, AuthenticationException $authException = null): Response
        {
            return new RedirectResponse('/connect/', Response::HTTP_TEMPORARY_REDIRECT);
        }
    }
  5. Configure a generic OAuth2 provider

    main

    If your OAuth server is not natively supported by the bundle, you can use the generic type to connect to a custom provider.

    1. Prepare a Provider Class: Ensure you have a provider class (e.g., one that extends AbstractProvider from the thephpleague/oauth2-client library) installed in your project.
    2. Configure in Symfony: Add the client to config/packages/knpu_oauth2_client.yaml using type: generic and specifying your provider_class.

    This will create a service named knpu.oauth2.client.[your_client_key] (e.g., knpu.oauth2.client.foo_bar_oauth).

    # config/packages/knpu_oauth2_client.yaml
    knpu_oauth2_client:
        clients:
            # will create service: "knpu.oauth2.client.foo_bar_oauth"
            foo_bar_oauth:
                type: generic
                provider_class: Some\Class\FooBarProvider
    
                # optional: a class that extends OAuth2Client
                # client_class: Some\Custom\Client
    
                # optional: if your provider has custom constructor options
                # provider_options: {}
    
                # now, all the normal options!
                client_id: '%env(foo_bar_client_id)%'
                client_secret: '%env(foo_bar_client_secret)%'
                redirect_route: connect_facebook_check
                redirect_params: {}
                # whether to check OAuth2 "state": defaults to true
                # use_state: true
  6. Configure the new Symfony Authenticator in security.yaml

    main

    To use the OAuth2Authenticator, register it under the custom_authenticators section of your security.yaml file.

    Note for Symfony 6.4 or lower: You must explicitly enable the authenticator manager by setting enable_authenticator_manager: true.

    # app/config/packages/security.yaml
    security:
        # ...  
        firewalls:
            # ...
            main:
            # ...
               custom_authenticators:
                   - App\Security\MyFacebookAuthenticator
    
    # For Symfony 6.4 or lower:
        enable_authenticator_manager: true
  7. Store and refresh OAuth2 access tokens

    main

    To use OAuth2 tokens later, you can choose between two primary storage strategies depending on whether you want to manage the full AccessToken object or just the refresh token string.

    Option 1: Store the AccessToken object

    By serializing the entire AccessToken object (e.g., into the Symfony session), you can use the $accessToken->hasExpired() method to check if a refresh is necessary before making an API call. This is efficient as it avoids unnecessary network requests.

    Option 2: Store the refresh token string

    Alternatively, you can store only the refresh token string (e.g., in a database field like user.refresh_token). In this scenario, you must call $client->refreshAccessToken() using the stored string to obtain a new AccessToken object. Note that when you refresh, you should also update your stored refresh token with the new one provided by the new AccessToken object.

    Passing extra parameters

    Some OAuth2 providers require specific parameters during the initial token acquisition or during the refresh process (for example, to request offline_access scopes). You can pass these as an associative array to the respective methods.

    // Option 1: Using the AccessToken object in a session
    $accessToken = $client->getAccessToken();
    $session->set('access_token', $accessToken);
    
    $accessToken = $session->get('access_token');
    if ($accessToken->hasExpired()) {
        $accessToken = $client->refreshAccessToken($accessToken->getRefreshToken());
        $session->set('access_token', $accessToken);
    }
    
    // Option 2: Using the refresh token string in a database
    $accessToken = $client->getAccessToken();
    $user->setRefreshToken($accessToken->getRefreshToken());
    $entityManager->flush();
    
    $accessToken = $client->refreshAccessToken($user->getRefreshToken());
    $user->setRefreshToken($accessToken->getRefreshToken());
    $entityManager->flush();
    
    // Using extra parameters (e.g., for scopes)
    $accessToken = $client->getAccessToken(['scopes' => 'offline_access']);
    $accessToken = $client->refreshAccessToken($accessToken->getRefreshToken(), ['scopes' => 'offline_access']);
  8. Use the ClientRegistry to start and handle OAuth2 flows

    main

    The KnpU\OAuth2ClientBundle\Client\ClientRegistry service is the primary way to interact with your configured clients.

    1. Starting the Redirect

    To initiate the OAuth process, retrieve your client by the key defined in your configuration and call redirect(). You can pass an array of scopes to request specific permissions.

    2. Handling the Callback

    In your callback controller (the route defined as redirect_route), use the client to fetch the user data. You should wrap this in a try-catch block to handle IdentityProviderException if the authentication fails.

    3. Fetching User Data

    Once the flow is complete, you can use the client to:

    • Fetch the user directly: $client->fetchUser()
    • Fetch the access token: $client->getAccessToken()
    • Fetch the user via a token: $client->fetchUserFromToken($accessToken)
    • Access the underlying League OAuth2 provider: $client->getOAuth2Provider()
    namespace App\Controller;
    
    use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
    use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Routing\Annotation\Route;
    
    class FacebookController extends AbstractController
    {
        /**
         * Step 1: Start the process
         * @Route("/connect/facebook", name="connect_facebook_start")
         */
        public function connectAction(ClientRegistry $clientRegistry)
        {
            return $clientRegistry
                ->getClient('facebook_main')
                ->redirect(['public_profile', 'email']);
        }
    
        /**
         * Step 2: Handle the callback
         * @Route("/connect/facebook/check", name="connect_facebook_check")
         */
        public function connectCheckAction(Request $request, ClientRegistry $clientRegistry)
        {
            /** @var \KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient $client */
            $client = $clientRegistry->getClient('facebook_main');
    
            try {
                /** @var \League\OAuth2\Client\Provider\FacebookUser $user */
                $user = $client->fetchUser();
                // Use $user here
            } catch (IdentityProviderException $e) {
                // Handle error
            }
        }
    }
  9. Install an OAuth2 Client Library

    main

    To use a specific OAuth2 provider (like GitHub, Facebook, or Google), you must first install the corresponding client library via Composer. The bundle acts as a wrapper around these libraries.

    Common providers and their installation commands:

    • Facebook: composer require league/oauth2-facebook
    • GitHub: composer require league/oauth2-github
    • Google: composer require league/oauth2-google
    • LinkedIn: composer require league/oauth2-linkedin
    • Discord: composer require wohali/oauth2-discord-new

    If your provider is not listed, check the league/oauth2-client providers list or configure a generic client.

    # Example: Installing Facebook client
    composer require league/oauth2-facebook
  10. Authenticate users with Guard (Legacy Symfony)

    main

    For older Symfony versions, use a Guard Authenticator. The bundle provides a SocialAuthenticator base class to simplify this process.

    Implementation Steps:

    1. Create an Authenticator: Extend KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator.
    2. Implement supports(): Return true if the request matches your OAuth callback route.
    3. Implement getCredentials(): Use $this->fetchAccessToken($this->getFacebookClient()) to retrieve the token.
    4. Implement getUser(): Use the credentials to fetch the user from the provider and resolve your local User entity.
    5. Register in Security: Add the authenticator under the guard.authenticators section in security.yaml.

    Using OAuthUserProvider for simple authentication

    If you do not need to persist custom user data, you can use the knpu.oauth2.user_provider service. This will log the user in as an instance of KnpU\OAuth2ClientBundle\Security\User\OAuthUser with the roles ROLE_USER and ROLE_OAUTH_USER.

    namespace App\Security;
    
    use App\Entity\User;
    use Doctrine\ORM\EntityManagerInterface;
    use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
    use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
    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\Core\User\UserProviderInterface;
    
    class MyFacebookAuthenticator extends SocialAuthenticator
    {
        private $clientRegistry;
        private $em;
        private $router;
    
        public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
        {
            $this->clientRegistry = $clientRegistry;
            $this->em = $em;
            $this->router = $router;
        }
    
        public function supports(Request $request)
        {
            return $request->attributes->get('_route') === 'connect_facebook_check';
        }
    
        public function getCredentials(Request $request)
        {
            return $this->fetchAccessToken($this->getFacebookClient());
        }
    
        public function getUser($credentials, UserProviderInterface $userProvider)
        {
            /** @var FacebookUser $facebookUser */
            $facebookUser = $this->getFacebookClient()->fetchUserFromToken($credentials);
            $email = $facebookUser->getEmail();
    
            $existingUser = $this->em->getRepository(User::class)->findOneBy(['facebookId' => $facebookUser->getId()]);
            if ($existingUser) {
                return $existingUser;
            }
    
            $user = $this->em->getRepository(User::class)->findOneBy(['email' => $email]);
            $user->setFacebookId($facebookUser->getId());
            $this->em->persist($user);
            $this->em->flush();
    
            return $user;
        }
    
        private function getFacebookClient()
        {
            return $this->clientRegistry->getClient('facebook_main');
        }
    
        public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
        {
            $targetUrl = $this->router->generate('app_homepage');
            return new RedirectResponse($targetUrl);
        }
    
        public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
        {
            $message = strtr($exception->getMessageKey(), $exception->getMessageData());
            return new Response($message, Response::HTTP_FORBIDDEN);
        }
    
        public function start(Request $request, AuthenticationException $authException = null)
        {
            return new RedirectResponse('/connect/', Response::HTTP_TEMPORARY_REDIRECT);
        }
    }
  11. Configure Azure OAuth2 Client

    main

    To use Azure, install thenetworg/oauth2-azure. This provider supports both client secrets and client certificates.

    Key configuration options:

    • type: Must be azure.
    • client_id: Your Azure client ID.
    • client_secret: The shared client secret (if not using a certificate).
    • client_certificate_private_key: The contents of the client certificate private key.
    • client_certificate_thumbprint: The hexadecimal thumbprint of the client certificate.
    • tenant: The tenant to use (defaults to common).
    • url_login: Domain to build the login URL (e.g., https://login.microsoftonline.com/).
    • url_api: Domain to build request URLs (e.g., https://graph.windows.net/).
    azure:
        type: azure
        client_id: '%env(OAUTH_AZURE_CLIENT_ID)%'
        redirect_route: connect_azure_check
        redirect_params: {}
        # client_secret: 'YOUR_SECRET'
        # client_certificate_private_key: '-----BEGIN RSA PRIVATE KEY-----
    ...'
        # tenant: 'common'
        use_state: true