OAuth2ServerBundle

repository·2.x·Indexed 42 days ago

https://github.com/thephpleague/oauth2-server-bundle

A Symfony bundle that integrates the 'thephpleague/oauth2-server' library to implement OAuth2 authorization servers. It provides CLI tools for managing clients, support for the Device Code Grant, and the ability to implement custom grant types. Features include Symfony security layer integration, scope-based route restriction, and automatic role mapping for token scopes.

Tokens
16.7K
Snippets
57
Records
64
Agent score
52%

What's inside OAuth2ServerBundle

  1. How the Device Code Grant works

    2.x

    The Device Code Grant is intended for devices with limited input capabilities (e.g., smart TVs, IoT devices) that lack a browser.

    The Flow:

    1. Initial Request: The device sends a request to /device-code containing its client_id and scope.
    2. Server Response: The server returns a device_code, a user_code, and a verification_url.
    3. User Interaction: The user visits the verification_url on a secondary device (like a smartphone) and enters the user_code.
    4. Polling: The original device continuously polls the /token endpoint using the device_code.
    5. Completion: Once the user approves the request on the secondary device, the /token endpoint returns the access token to the original device.
  2. How to handle the Password Grant via UserResolveEvent

    2.x

    The password grant issues access and refresh tokens bound to both a client and a user. Because user authentication logic varies between applications, the bundle provides the league.oauth2_server.event.user_resolve event.

    To implement custom user resolution, you must create an event listener that listens for this event. Inside the listener, you are responsible for:

    1. Retrieving the user from your user provider using the username provided in the event.
    2. Verifying that the user implements Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface.
    3. Validating the provided password against the user's stored hash.
    4. Calling $event->setUser($user) to bind the authenticated user to the token issuance process.

    If the credentials are invalid or the user cannot be found, simply return from the listener without calling setUser().

    <?php
    
    namespace App\EventListener;
    
    use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
    use Symfony\Component\Security\Core\User\UserProviderInterface;
    use Symfony\Component\Security\Core\Exception\AuthenticationException;
    use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
    use League\Bundle\OAuth2ServerBundle\Event\UserResolveEvent;
    
    final class UserResolveListener
    {
        private $userProvider;
        private $userPasswordHasher;
    
        public function __construct(UserProviderInterface $userProvider, UserPasswordHasherInterface $userPasswordHasher)
        {
            $this->userProvider = $userProvider;
            $this->userPasswordHasher = $userPasswordHasher;
        }
    
        public function onUserResolve(UserResolveEvent $event): void
        {
            try {
                $user = $this->userProvider->loadUserByIdentifier($event->getUsername());
            } catch (AuthenticationException $e) {
                return;
            }
    
            if (null === $user || !($user instanceof PasswordAuthenticatedUserInterface)) {
                return;
            }
    
            if (!$this->userPasswordHasher->isPasswordValid($user, $event->getPassword())) {
                return;
            }
    
            $event->setUser($user);
        }
    }
  3. Use OAuth2 scopes as Security roles

    2.x

    When a user authenticates, they are granted roles based on their token scopes. By default, these roles follow the pattern ROLE_OAUTH2_<scope>. You can use these roles with Symfony's @IsGranted annotation to protect controller actions.

    You can customize the ROLE_OAUTH2_ prefix using the role_prefix configuration option.

    /**
     * @IsGranted("ROLE_OAUTH2_EDIT")
     */
    public function indexAction()
    {
        // ...
    }
  4. Implement the Device Verification UI

    2.x

    The bundle does not provide a built-in route or UI for user code input. You must implement the verification logic yourself.

    To complete a device authorization request, you should:

    1. Retrieve the device code using the DeviceCodeManagerInterface::findByUserCode() method.
    2. Call AuthorizationServer::completeDeviceAuthorizationRequest() with the device code identifier, the authenticated user's ID, and a boolean indicating approval.

    Note: Ensure you handle OAuthServerException cases, such as invalid codes or missing user IDs.

    // Inside your controller handling the verification form
    try {
        $deviceCode = $this->deviceCodeManager->findByUserCode($form->get('userCode')->getData());
        if (null === $deviceCode) {
            throw OAuthServerException::invalidGrant('Invalid user code');
        }
        
        $this->authorizationServer->completeDeviceAuthorizationRequest(
            $deviceCode->getIdentifier(),
            $this->getUser()->getId(),
            true // User has approved the device
        );
    } catch (OAuthServerException $e) {
        // Handle exception
    }
  5. Install the OAuth2 Server Bundle

    2.x

    To install the bundle in a Symfony project, use Composer. If you are using Symfony Flex, the bundle will be automatically registered, and you can skip the manual bundle enablement step.

    Manual Installation Steps

    1. Install via Composer.
    2. Enable the bundle in config/bundles.php.
    3. Update your database schema to support Doctrine persistence.
    4. Import the OAuth2 routes into your application.
    # 1. Install the bundle
    composer require league/oauth2-server-bundle
    
    # 3. Update database schema
    bin/console doctrine:schema:update --force
  6. Alter issued access token scopes using events

    2.x

    You can dynamically modify the scopes assigned to an access token by subscribing to the league.oauth2_server.event.scope_resolve event.

    To implement this:

    1. Create an event listener that accepts a League\Bundle\OAuth2ServerBundle\Event\ScopeResolveEvent object.
    2. Use $event->getScopes() to retrieve the currently requested scopes.
    3. Perform your logic to add, remove, or modify scopes.
    4. Use $event->setScopes(...) to apply the changes.
    5. Register the listener in your service configuration using the kernel.event_listener tag, targeting the league.oauth2_server.event.scope_resolve event.
    <?php
    
    namespace App\EventListener;
    
    use League\Bundle\OAuth2ServerBundle\Event\ScopeResolveEvent;
    
    final class ScopeResolveListener
    {
        public function onScopeResolve(ScopeResolveEvent $event): void
        {
            $requestedScopes = $event->getScopes();
    
            // ...Make adjustments to the client's requested scopes...
    
            $event->setScopes(...$requestedScopes);
        }
    }
    App\EventListener\ScopeResolveListener:
        tags:
            - { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve }
  7. Implement a custom grant type

    2.x

    To implement a custom grant type, create a class that implements the League\OAuth2\Server\Grant\GrantTypeInterface. It is recommended to extend League\OAuth2\Server\Grant\AbstractGrant to simplify the implementation.

    Your class must implement:

    • getIdentifier(): Returns the unique string identifier for the grant (e.g., 'fake_grant').
    • respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL): Handles the logic for responding to an access token request.
    <?php
    
    declare(strict_types=1);
    
    namespace App\Grant;
    
    use DateInterval;
    use League\OAuth2\Server\Grant\AbstractGrant;
    use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
    use Nyholm\Psr7\Response;
    use Psr\Http\Message\ServerRequestInterface;
    use League\OAuth2\Server\Grant\GrantTypeInterface;
    
    final class FakeGrant extends AbstractGrant implements GrantTypeInterface
    {
        private $foo;
    
        public function __construct(SomeDependency $foo)
        {
            $this->foo = $foo;
        }
    
        public function getIdentifier()
        {
            return 'fake_grant';
        }
    
        public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL)
        {
            return new Response();
        }
    }
  8. Grant all scopes via role hierarchy workaround

    2.x

    If you require clients with no specified scopes to have access to all available scopes while still using the default scope mechanism, you can use Symfony's role hierarchy.

    1. Set a role_prefix in the bundle configuration (e.g., ROLE_OAUTH2_).
    2. Define a default scope that represents a 'super user' scope.
    3. Map that super user scope to all other available scopes in your security.yaml role hierarchy.
    # config/packages/league_oauth2_server.yaml
    
    league_oauth2_server:
        role_prefix: ROLE_OAUTH2_
    
        scopes:
            available: [EMAIL, PREFERENCES, SUPER_USER]
            default: [SUPER_USER]
    # config/packages/security.yaml
    security:
        role_hierarchy:
            ROLE_OAUTH2_SUPER_USER: [ROLE_OAUTH2_EMAIL, ROLE_OAUTH2_PREFERENCES]
  9. Implement a custom Client entity

    2.x

    To use your own domain model for OAuth2 clients instead of the default bundle implementation, create a class that extends \League\Bundle\OAuth2ServerBundle\Model\AbstractClient. This allows you to integrate the client with your own persistence layer, such as Doctrine ORM, and add custom properties (e.g., an image URL or other metadata) to your client entity.

    <?php
    
    declare(strict_types=1);
    
    namespace App\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    use League\Bundle\OAuth2ServerBundle\Model\AbstractClient;
    
    #[ORM\Entity]
    class Client extends AbstractClient
    {
        #[ORM\Id]
        #[ORM\Column(type: 'string', length: 32)]
        protected $identifier;
    
        #[ORM\Column(type: 'string')]
        private $image;
    
        // other properties, getters, setters, ...
    }
  10. Enable the Device Code Grant

    2.x

    The device code grant is disabled by default. To use it, you must explicitly enable it in your configuration. Enabling this grant requires the oauth2_device_code table to be present in your database schema.

    You must also define the device_code_verification_uri, which is the URL where users will go to enter their codes.

    league_oauth2_server:
        authorization_server:
            enable_device_code_grant: true
            device_code_verification_uri: 'https://your-domain.com/verify-device'