oauth2-bundle icon indicating copy to clipboard operation
oauth2-bundle copied to clipboard

Add custom parameters in response

Open lud opened this issue 5 years ago • 6 comments

Hi,

We need to provide the user id in the response issued when handling password grant request.

I found this code in the league library:

    /**
     * Add custom fields to your Bearer Token response here, then override
     * AuthorizationServer::getResponseType() to pull in your version of
     * this class rather than the default.
     *
     * @param AccessTokenEntityInterface $accessToken
     *
     * @return array
     */
    protected function getExtraParams(AccessTokenEntityInterface $accessToken)
    {
        return [];
    }

Now, I would like to know how I can override the AuthorizationServer class to pass a instance of a custom child class. So that object can return a custom response type, which in its turn would add my custom parameters to the response.

Thank you.

lud avatar Nov 24 '20 19:11 lud

Or I could add a custom grant, and override respondToAccessTokenRequest from the PasswordGrant ? That would be better since that is where we have the full user instance (so no need to inject the user repository somewhere else). But that means that I do not return the same response type as passed to respondToAccessTokenRequest.

lud avatar Nov 24 '20 19:11 lud

If someone has the needing to customize the Bearer token's content here's how I bypassed the limitation:

  1. override services with my own repository:

services.yaml

Trikoder\Bundle\OAuth2Bundle\League\Repository\AccessTokenRepository:
        class: App\Repository\AccessTokenRepository
 League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface:
        alias: Trikoder\Bundle\OAuth2Bundle\League\Repository\AccessTokenRepository
  1. copy paste Trikoder\Bundle\OAuth2Bundle\League\Repository\AccessTokenRepository in custom class App\Repository\AccessTokenRepository (cannot extend because of final)

  2. Create a custom App\Entity\AccessTokenClass to override AccessTokenTrait

App\Entity\AccessTokenClass

namespace App\Entity;

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;

final class AccessToken implements AccessTokenEntityInterface {

	private $claims = [];

	use AccessTokenTrait;
	use EntityTrait;
	use TokenEntityTrait;

	private function convertToJWT() {
		$this->initJwtConfiguration();

		$builder = $this->jwtConfiguration->builder()
		                                  ->permittedFor($this->getClient()->getIdentifier())
		                                  ->identifiedBy($this->getIdentifier())
		                                  ->issuedAt(new \DateTimeImmutable())
		                                  ->canOnlyBeUsedAfter(new \DateTimeImmutable())
		                                  ->expiresAt($this->getExpiryDateTime())
		                                  ->relatedTo((string)$this->getUserIdentifier())
		                                  ->withClaim('scopes', $this->getScopes());
		foreach ($this->claims as $claim) {
			$builder->withClaim($claim['key'], $claim['value']);
		}

		return $builder->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
	}

	/**
	 * Generate a string representation from the access token
	 */
	public function __toString() {
		return $this->convertToJWT()->toString();
	}

	public function addClaim($claim) {
		$this->claims[] = $claim;
	}
}
  1. change the import AccessTokenClass in App\Repository\AccessTokenRepository with

use App\Entity\AccessToken as AccessTokenEntity;

So the full code of App\Repository\AccessTokenRepository it's something like:

<?php

declare(strict_types=1);

namespace App\Repository;

use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use Trikoder\Bundle\OAuth2Bundle\Converter\ScopeConverterInterface;
use App\Entity\AccessToken as AccessTokenEntity;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken as AccessTokenModel;

final class AccessTokenRepository implements AccessTokenRepositoryInterface
{
	/**
	 * @var AccessTokenManagerInterface
	 */
	private $accessTokenManager;

	/**
	 * @var ClientManagerInterface
	 */
	private $clientManager;

	/**
	 * @var ScopeConverterInterface
	 */
	private $scopeConverter;

	public function __construct(
		AccessTokenManagerInterface $accessTokenManager,
		ClientManagerInterface $clientManager,
		ScopeConverterInterface $scopeConverter
	) {
		$this->accessTokenManager = $accessTokenManager;
		$this->clientManager = $clientManager;
		$this->scopeConverter = $scopeConverter;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
	{
		$accessToken = new AccessTokenEntity();
		$accessToken->setClient($clientEntity);
		$accessToken->setUserIdentifier($userIdentifier);
		$accessToken->addClaim(['key' => 'my key', 'value' => 'my value']); // Here we add the custom claim

		foreach ($scopes as $scope) {
			$accessToken->addScope($scope);
		}

		return $accessToken;
	}

	/**
	 * {@inheritdoc}
	 */
	public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity)
	{
		$accessToken = $this->accessTokenManager->find($accessTokenEntity->getIdentifier());

		if (null !== $accessToken) {
			throw UniqueTokenIdentifierConstraintViolationException::create();
		}

		$accessToken = $this->buildAccessTokenModel($accessTokenEntity);

		$this->accessTokenManager->save($accessToken);
	}

	/**
	 * {@inheritdoc}
	 */
	public function revokeAccessToken($tokenId)
	{
		$accessToken = $this->accessTokenManager->find($tokenId);

		if (null === $accessToken) {
			return;
		}

		$accessToken->revoke();

		$this->accessTokenManager->save($accessToken);
	}

	/**
	 * {@inheritdoc}
	 */
	public function isAccessTokenRevoked($tokenId)
	{
		$accessToken = $this->accessTokenManager->find($tokenId);

		if (null === $accessToken) {
			return true;
		}

		return $accessToken->isRevoked();
	}

	private function buildAccessTokenModel(AccessTokenEntityInterface $accessTokenEntity): AccessTokenModel
	{
		$client = $this->clientManager->find($accessTokenEntity->getClient()->getIdentifier());

		return new AccessTokenModel(
			$accessTokenEntity->getIdentifier(),
			$accessTokenEntity->getExpiryDateTime(),
			$client,
			$accessTokenEntity->getUserIdentifier(),
			$this->scopeConverter->toDomainArray($accessTokenEntity->getScopes())
		);
	}
}

If you need to get those keys and evaluate in response you just need to use a custom guard as explained in the documentation like:

security.yaml

api_oauth2:
            pattern: ^/api/oauth2
            security: true
            stateless: true
            guard:
                authenticators:
                    - App\Security\MyCustomAuthenticator

and inside MyCustomAuthenticator use the normal flow for symfony authentication.

Hope this helps someone!

Stefanobosio avatar Mar 11 '21 08:03 Stefanobosio

Hi @Stefanobosio , thank for your answer. This is what we have done:

In our Kernel we are adding a compilation pass:

  protected function build(ContainerBuilder $container): void
    {
        // ...
        $container->addCompilerPass(new ExtendAuthorizationServerPass());
        // ...        
    }

We define the pass as:

class ExtendAuthorizationServerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // Override the base AuthorizationServer
        $container
            ->getDefinition(AuthorizationServer::class)
            ->setClass(AuthorizationServerExtension::class)
            ->setAutowired(true);
    }
}

In the AuthorizationServerExtension (that extends the AuthorizationServer class) __construct function we call the parent constructor with our extended type of response:

        parent::__construct(
            $clientRepository,
            $accessTokenRepository,
            $scopeRepository,
            $privateKey,
            $encryptionKey,
            new BearerTokenResponseWithMetaData(/* some custom services */) // <---- * here *
        );

And finally in the BearerTokenResponseWithMetaData class we can define the protected function getExtraParams(AccessTokenEntityInterface $accessToken) and return whatever data we want to.

Note that does't allow to modify the access token in all manners, but just the attached metadata, which should be sufficient for most needs.

lud avatar Mar 11 '21 09:03 lud

Hello @lud, thank you for your reply. Actually getExtraParams() doesn't add any claim to the token but it adds extra keys to the json response. What I'm looking for is to change the bearer's content adding new claim to the standard ones before it's generated. I need this in order to validate extra information during the JWT decoding. And I managed to do that with the code in my answer below, but it's more a kind of hack than a real solution. Did you maybe encounter this need too and already found a solution?

Stefanobosio avatar Mar 11 '21 15:03 Stefanobosio

Sorry I have never needed to to that. If we need more data we pass it in the metadata.

lud avatar Mar 14 '21 23:03 lud

Sure, no problem. I need data to be validated in Bearer instead of passing them directly in the response. Anyway, my solution above works and I could temporary solve the problem.

Thank you for your help!

Stefanobosio avatar Mar 15 '21 09:03 Stefanobosio