Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do you add an extra claim? #198

Open
MartijnBUZ opened this issue Sep 5, 2024 · 2 comments
Open

How do you add an extra claim? #198

MartijnBUZ opened this issue Sep 5, 2024 · 2 comments

Comments

@MartijnBUZ
Copy link

MartijnBUZ commented Sep 5, 2024

I get a working JWT token, but I want to add extra data to it:

{
 "...": "...",
  "sub": "myname",
  "scopes": [
    "messaging"
  ],
  "my-own-added-key": "this is a neat custom value"
}

I've found Lcobucci\JWT\Builder::withClaim in the code which seems exactly what I need, but there is no way for my to apply it. For some reason everything is final, so I can't apply the Open/Closed principle anywhere. I'm not looking to rewrite half this tool, I just need a small hook to add a little data.

  • The events that exists (OAuth2Events) dont offer anything useful.
  • Altering the build cant, its incorrectly final anyway.
  • cant create a custom AccessToken, the current one is incorrectly final
  • None of the managers are usefull
  • None of the builds can be configured (and are incorrectly marked as final)
  • There is no way to alter an accesstoken to have something like $customClaims which could be picked up in the AccessTokenTrait again.
  • 'RelatedTo' must be a string. Using a simplified array here would solve a lot

Is there a reason this is so hard? I dont mind creating some code to implement this, but I'd like to know if that is worth my time.

@MartijnBUZ
Copy link
Author

I've been searching for a subtle way to implement this, but this turns out to be a bit challenging. The AccessTokenTrait::convertToJwt has service (or manager) logic, but is placed in a entity structure. IMO an entity should just be the definition of a Thing. Building and doing stuff is service (or manager) logic.

Something like a league.oauth2_server.event.token_creation_claims_added would be perfect. Or just something in the builder I could hook to. But now it instantly goes to __tostring.

@LiamBdr
Copy link

LiamBdr commented Apr 11, 2025

Late reply, but I encountered the same challenge when trying to add custom claims to the JWT generated by this bundle.

As you suggested, dispatching an event before the token is finalized seems like the most appropriate way to introduce a hook without significantly altering the core logic.

I came up with a potential solution by introducing a new BEFORE_JWT_TOKEN_BUILD event. It might not be the perfect approach, so feel free to share your thoughts. Here’s how it works:

  1. Define the Event Constant:
    Add the new event constant in /src/OAuth2Events.php:

    // src/OAuth2Events.php
    
    // ... (other constants)
    
    /**
     * The BEFORE_JWT_TOKEN_BUILD event is dispatched just before the JWT token is built by the BearerTokenResponse.
     *
     * You can use this event to add custom claims to the JWT builder.
     * The event listener receives a League\Bundle\OAuth2ServerBundle\Event\BeforeJwtTokenBuildEvent instance.
     *
     * @Event("League\Bundle\OAuth2ServerBundle\Event\BeforeJwtTokenBuildEvent")
     */
    public const BEFORE_JWT_TOKEN_BUILD = 'league.oauth2_server.event.before_jwt_token_build';
  2. Create the Event Class:
    Create the corresponding event class BeforeJwtTokenBuildEvent. This class holds the JWT Builder instance and the AccessTokenEntityInterface, allowing listeners to access them. Since lcobucci/jwt builders are immutable, the class exposes a setBuilder() method to handle the modified instance:

    // src/Event/BeforeJwtTokenBuildEvent.php
    <?php
    
    declare(strict_types=1);
    
    namespace League\Bundle\OAuth2ServerBundle\Event;
    
    use Lcobucci\JWT\Token\Builder as BuilderInterface;
    use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
    use Symfony\Contracts\EventDispatcher\Event;
    
    final class BeforeJwtTokenBuildEvent extends Event
    {
        public function __construct(
            private BuilderInterface $builder,
            private readonly AccessTokenEntityInterface $accessToken
        ) {
        }
    
        public function getBuilder(): BuilderInterface
        {
            return $this->builder;
        }
    
        public function getAccessToken(): AccessTokenEntityInterface
        {
            return $this->accessToken;
        }
    
        public function setBuilder(BuilderInterface $builder): void
        {
            $this->builder = $builder;
        }
    }
  3. Inject EventDispatcher Statically:
    Since the JWT creation logic is located in AccessToken::convertToJWT(), we can’t use regular DI. Instead, inject the dispatcher statically in the bundle’s boot() method:

    // src/LeagueOAuth2ServerBundle.php
    use League\Bundle\OAuth2ServerBundle\Entity\AccessToken;
    use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
    
    public function boot(): void
    {
        parent::boot();
    
        if ($this->container && $this->container->has('event_dispatcher')) {
             /** @var EventDispatcherInterface $eventDispatcher */
             $eventDispatcher = $this->container->get('event_dispatcher');
    
             if (class_exists(AccessToken::class) && method_exists(AccessToken::class, 'setEventDispatcher')) {
                 AccessToken::setEventDispatcher($eventDispatcher);
             }
        }
    }
  4. Dispatch the Event in AccessToken:
    Modify /src/Entity/AccessToken.php to add the static property/setter for the dispatcher and dispatch the event within convertToJWT.

    // src/Entity/AccessToken.php
    <?php
    
    declare(strict_types=1);
    
    namespace League\Bundle\OAuth2ServerBundle\Entity;
    
    use Lcobucci\JWT\Token;
    use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
    use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
    use League\OAuth2\Server\Entities\Traits\EntityTrait;
    use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
    use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
    use DateTimeImmutable;
    use League\Bundle\OAuth2ServerBundle\Event\BeforeJwtTokenBuildEvent;
    use League\Bundle\OAuth2ServerBundle\OAuth2Events;
    
    class AccessToken implements AccessTokenEntityInterface
    {
        use AccessTokenTrait;
        use EntityTrait;
        use TokenEntityTrait;
    
        private static ?EventDispatcherInterface $eventDispatcher = null;
    
        public static function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
        {
            self::$eventDispatcher = $eventDispatcher;
        }
    
        private function convertToJWT(): Token
        {
            $this->initJwtConfiguration();
            $builder = $this->jwtConfiguration->builder();
    
            if (self::$eventDispatcher) {
                $event = new BeforeJwtTokenBuildEvent($builder, $this);
                self::$eventDispatcher->dispatch($event, OAuth2Events::BEFORE_JWT_TOKEN_BUILD);
                $builder = $event->getBuilder();
            }
    
            return $builder
                ->permittedFor($this->getClient()->getIdentifier())
                ->identifiedBy($this->getIdentifier())
                ->issuedAt(new DateTimeImmutable())
                ->canOnlyBeUsedAfter(new DateTimeImmutable())
                ->expiresAt($this->getExpiryDateTime())
                ->relatedTo($this->getSubjectIdentifier())
                ->withClaim('scopes', $this->getScopes())
                ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
        }
    }
  5. Usage Example (Event Listener):
    Here’s how you can listen to the event and inject custom claims:

    <?php
    declare(strict_types=1);
    
    namespace App\EventSubscriber;
    
    use League\Bundle\OAuth2ServerBundle\Event\BeforeJwtTokenBuildEvent;
    use League\Bundle\OAuth2ServerBundle\OAuth2Events;
    use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
    
    #[AsEventListener(event: OAuth2Events::BEFORE_JWT_TOKEN_BUILD)]
    final readonly class JwtSimpleSubscriberExample
    {
        public function __invoke(BeforeJwtTokenBuildEvent $event): void
        {
            $builder = $event->getBuilder();
    
            $newBuilder = $builder->withClaim('custom_claim', 'yeah');
            $newBuilder = $newBuilder->withClaim('another_claim', 'some_data');
    
            $event->setBuilder($newBuilder);
    
            // More concise example:
            // $event->setBuilder(
            //     $event->getBuilder()->withClaim('my-own-added-key', 'custom value')
            // );
        }
    }

Let me know if you see any downsides or potential improvements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants