Add custom parameters in response
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.
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.
If someone has the needing to customize the Bearer token's content here's how I bypassed the limitation:
- 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
-
copy paste Trikoder\Bundle\OAuth2Bundle\League\Repository\AccessTokenRepository in custom class App\Repository\AccessTokenRepository (cannot extend because of final)
-
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;
}
}
- 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!
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.
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?
Sorry I have never needed to to that. If we need more data we pass it in the metadata.
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!