Skip to content

Instantly share code, notes, and snippets.

@ajgarlag
Last active October 23, 2025 12:55
Show Gist options
  • Save ajgarlag/1f84d29ee0e1a92c8878f44a902338cd to your computer and use it in GitHub Desktop.
Save ajgarlag/1f84d29ee0e1a92c8878f44a902338cd to your computer and use it in GitHub Desktop.
Simple league/oauth2-bundle decision flow
{# templates/oauth2/authorize_decision.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<dl>
<dt><strong>Client Name:</strong></dt>
<dd>{{ client.name }}</dd>
<dt><strong>Redirect URI:</strong></dt>
<dd>{{ redirect_uri }}</dd>
<dt><strong>Requested scopes:</strong></dt>
{% for scope in scopes %}
<dd>{{ scope }}</dd>
{% endfor %}
</dl>
<div>
<a href="{{ allow_uri }}">Allow</a> | <a href="{{ deny_uri }}">Deny</a>
</div>
{% endblock %}
<?php
//src/Controller/DecisionController.php
namespace App\Controller;
use App\EventSubscriber\SignedAuthorizationRequestSubscriber;
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\ClientManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class DecisionController extends AbstractController
{
public function __construct(
private readonly UriSigner $uriSigner,
private readonly ClientManagerInterface $clientManager,
private readonly string $authorizationRoute,
) {
}
#[Route('/oauth2/authorize/decision', name: 'oauth2_authorize_decision')]
#[IsGranted('ROLE_USER')]
public function __invoke(Request $request)
{
Request $request,
#[MapQueryParameter('client_id')] string $clientId,
#[MapQueryParameter('redirect_uri')] string $redirectUri,
#[MapQueryParameter('scope')] string $scope = '',
): Response {
$client = $this->clientManager->find($clientId);
if (null === $client) {
throw new BadRequestHttpException();
}
$scopes = '' === $scope ? array_map(strval(...), $client->getScopes()) : explode(' ', $scope);
return $this->render('oauth2/authorize_decision.html.twig', [
'client' => $client,
'redirect_uri' => $redirectUri,
'scopes' => $scopes,
'allow_uri' => $this->buildDecidedUri($request, true),
'deny_uri' => $this->buildDecidedUri($request, false),
]);
}
private function buildDecidedUri(Request $request, bool $allowed)
{
$currentQuery = $request->query->all();
$decidedQuery = array_merge($currentQuery, [SignedAuthorizationRequestSubscriber::ATTRIBUTE_DECISION => $this->buildDecisionValue($allowed)]);
$decidedUri = $this->generateUrl($this->authorizationRoute, $decidedQuery);
return $this->uriSigner->sign($decidedUri);
}
private function buildDecisionValue(bool $allowed): string
{
return $allowed ? SignedAuthorizationRequestSubscriber::ATTRIBUTE_DECISION_ALLOW : '';
}
}
<?php
//src/EventSubscriber/SignedAuthorizationRequestSubscriber.php
namespace App\EventSubscriber;
use League\Bundle\OAuth2ServerBundle\Event\AuthorizationRequestResolveEvent;
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
use League\Bundle\OAuth2ServerBundle\OAuth2Grants;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SignedAuthorizationRequestSubscriber implements EventSubscriberInterface
{
public const ATTRIBUTE_DECISION = 'decision';
public const ATTRIBUTE_DECISION_ALLOW = 'allow';
public function __construct(
private readonly UriSigner $uriSigner,
private readonly RequestStack $requestStack,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly string $decisionRoute = 'oauth2_decide',
) {
}
public function processSignedAuthorizationRequest(AuthorizationRequestResolveEvent $event): void
{
if (null === $request = $this->requestStack->getMainRequest()) {
return;
}
$currentUri = $request->getRequestUri();
if (!$this->uriSigner->check($currentUri)) {
return;
}
if (!$this->canResolveAuthorizationRequest($event, $request)) {
return;
}
$event->resolveAuthorization($this->isAuthorizationAllowed($request));
}
private function canResolveAuthorizationRequest(AuthorizationRequestResolveEvent $event, Request $request)
{
if (!$request->query->has(self::ATTRIBUTE_DECISION)) {
return false;
}
if ($request->query->get('client_id') !== $event->getClient()->getIdentifier()) {
return false;
}
if ($request->query->get('response_type') !== $this->getResponseType($event)) {
return false;
}
if ($request->query->get('redirect_uri') !== $event->getRedirectUri()) {
return false;
}
if ($request->query->get('scope') !== $this->getScope($event)) {
return false;
}
return true;
}
private function getResponseType(AuthorizationRequestResolveEvent $event): string
{
switch ($event->getGrantTypeId()) {
case OAuth2Grants::AUTHORIZATION_CODE:
return 'code';
case OAuth2Grants::IMPLICIT:
return 'token';
default:
return $event->getGrantTypeId();
}
}
private function getScope(AuthorizationRequestResolveEvent $event): ?string
{
$scopes = $event->getScopes();
if (empty($scopes)) {
return null;
}
return implode(' ', array_map('strval', $scopes));
}
private function isAuthorizationAllowed(Request $request): bool
{
return self::ATTRIBUTE_DECISION_ALLOW === $request->get(self::ATTRIBUTE_DECISION);
}
public function redirectToDecisionRoute(AuthorizationRequestResolveEvent $event): void
{
$params = [
'client_id' => $event->getClient()->getIdentifier(),
'response_type' => $this->getResponseType($event),
];
if (null !== $redirectUri = $event->getRedirectUri()) {
$params['redirect_uri'] = $redirectUri;
}
if (null !== $state = $event->getState()) {
$params['state'] = $state;
}
$scope = $this->getScope($event);
if (null !== $scope) {
$params['scope'] = $scope;
}
$event->setResponse(
new RedirectResponse(
$this->urlGenerator->generate($this->decisionRoute, $params)
)
);
}
public static function getSubscribedEvents(): array
{
return [
OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE => [
['processSignedAuthorizationRequest', 100],
['redirectToDecisionRoute', 50],
],
];
}
}
@gustvao
Copy link

gustvao commented Sep 22, 2019

hey @ajgarlag , what did you put in your services.yaml to make this work?

I am receiving the following error, any color on what may be causing it?

Cannot autowire service "App\Application\Service\OAuth2\SignedAuthorizationRequestSubscriber": argument "$uriSigner" of method "__construct()" references class "Symfony\Component\HttpKernel\UriSigner" but no such service exists. You should maybe alias this class to the existing "uri_signer" service.```

@ajgarlag
Copy link
Author

ajgarlag commented Sep 23, 2019

You have two options:

@gustvao
Copy link

gustvao commented Sep 23, 2019

Ok, that worked.
this is what I added to services.yaml

App\Application\Service\OAuth2\SignedAuthorizationRequestSubscriber:
        arguments:
            $uriSigner: 'bla'
            $decisionRoute: 'http://127.0.0.1:8000'
        tags:
            - { name: kernel.event_listener, event: trikoder.oauth2.authorization_request_resolve, method: processSignedAuthorizationRequest }

However when I hit /authorize method processSignedAuthorizationRequest never gets called.

If you could provide the working example it would be awesome.

Tks again

@gustvao
Copy link

gustvao commented Sep 23, 2019

Sorry, that $uriSigner: 'bla' is not working =/

"Argument 1 passed to App\\Application\\Service\\OAuth2\\SignedAuthorizationRequestSubscriber::__construct() must be an instance of Symfony\\Component\\HttpKernel\\UriSigner, string given, called in //var/cache/dev/ContainerHKNUUVZ/getSignedAuthorizationRequestSubscriberService.php on line 11"
}

@zhukovsergei
Copy link

Hi, Why you have duplicate keys in array 151, 152 lines on SignedAuthorizationRequestSubscriber?

@ajgarlag
Copy link
Author

@zhukovsergei It's a bug, it should be:

return [
    OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE => [
        ['processSignedAuthorizationRequest', 100],
        ['redirectToDecisionRoute', 50],
    ],
];

@fishmandev
Copy link

fishmandev commented Nov 10, 2020

@ajgarlag, https://gist.github.com/ajgarlag/1f84d29ee0e1a92c8878f44a902338cd#file-signedauthorizationrequestsubscriber-php-L141
Argument must implement interface Psr\Http\Message\ResponseInterface, instead of Symfony\Component\HttpFoundation\RedirectResponse

Symfony: 5.1.18

@fishmandev
Copy link

The question is closed.
$this->container->get('security.token_storage')->getToken()->getUser()

@mssoylu
Copy link

mssoylu commented Feb 3, 2021

@fishmandev how did you fix it?

@ajgarlag do you have any idea?

I guess it's about PSR standart for Symfony5. $event only accept PSR ResponseInterface but not HTTPFoundation based RedirectResponse.

@vatoer
Copy link

vatoer commented May 28, 2021

to solve error
Argument must implement interface Psr\Http\Message\ResponseInterface, instead of Symfony\Component\HttpFoundation\RedirectResponse

do
composer require nyholm/psr7

add this on the top

//src/EventListener/SignedAuthorizationRequestSubscriber.ph
use Nyholm\Psr7\Response;

change this line
https://gist.github.com/ajgarlag/1f84d29ee0e1a92c8878f44a902338cd#file-signedauthorizationrequestsubscriber-php-L141

$event->setResponse(
            new RedirectResponse(
                $this->urlGenerator->generate($this->decisionRoute, $params)
            )
        );

to

$url = $this->urlGenerator->generate($this->decisionRoute, $params);
$headers = ["Location"=>$url];
$response = new Response(301,$headers);
$event->setResponse($response);

hope this help, even a bit late @mssoylu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment