diff --git a/Controller/AuthorizationController.php b/Controller/AuthorizationController.php
index 68253057..9d2c4ce2 100644
--- a/Controller/AuthorizationController.php
+++ b/Controller/AuthorizationController.php
@@ -7,7 +7,6 @@
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ResponseFactoryInterface;
-use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Trikoder\Bundle\OAuth2Bundle\Converter\UserConverterInterface;
@@ -15,6 +14,7 @@
use Trikoder\Bundle\OAuth2Bundle\Event\AuthorizationRequestResolveEventFactory;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\OAuth2Events;
+use Trikoder\Bundle\OAuth2Bundle\Security\Exception\ExceptionEventFactory;
final class AuthorizationController
{
@@ -43,21 +43,28 @@ final class AuthorizationController
*/
private $clientManager;
+ /**
+ * @var ExceptionEventFactory
+ */
+ private $exceptionEventFactory;
+
public function __construct(
AuthorizationServer $server,
EventDispatcherInterface $eventDispatcher,
AuthorizationRequestResolveEventFactory $eventFactory,
UserConverterInterface $userConverter,
- ClientManagerInterface $clientManager
+ ClientManagerInterface $clientManager,
+ ExceptionEventFactory $exceptionEventFactory
) {
$this->server = $server;
$this->eventDispatcher = $eventDispatcher;
$this->eventFactory = $eventFactory;
$this->userConverter = $userConverter;
$this->clientManager = $clientManager;
+ $this->exceptionEventFactory = $exceptionEventFactory;
}
- public function indexAction(ServerRequestInterface $serverRequest, ResponseFactoryInterface $responseFactory): ResponseInterface
+ public function indexAction(ServerRequestInterface $serverRequest, ResponseFactoryInterface $responseFactory)
{
$serverResponse = $responseFactory->createResponse();
@@ -90,7 +97,8 @@ public function indexAction(ServerRequestInterface $serverRequest, ResponseFacto
return $this->server->completeAuthorizationRequest($authRequest, $serverResponse);
} catch (OAuthServerException $e) {
- return $e->generateHttpResponse($serverResponse);
+ $event = $this->exceptionEventFactory->handleLeagueException($e);
+ return $event->getResponse();
}
}
}
diff --git a/Controller/TokenController.php b/Controller/TokenController.php
index 6d78619e..3fe8aae1 100644
--- a/Controller/TokenController.php
+++ b/Controller/TokenController.php
@@ -7,8 +7,8 @@
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ResponseFactoryInterface;
-use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Trikoder\Bundle\OAuth2Bundle\Security\Exception\ExceptionEventFactory;
final class TokenController
{
@@ -17,21 +17,28 @@ final class TokenController
*/
private $server;
- public function __construct(AuthorizationServer $server)
+ /**
+ * @var ExceptionEventFactory
+ */
+ private $exceptionEventFactory;
+
+ public function __construct(AuthorizationServer $server, ExceptionEventFactory $exceptionEventFactory)
{
$this->server = $server;
+ $this->exceptionEventFactory = $exceptionEventFactory;
}
public function indexAction(
ServerRequestInterface $serverRequest,
ResponseFactoryInterface $responseFactory
- ): ResponseInterface {
+ ) {
$serverResponse = $responseFactory->createResponse();
try {
return $this->server->respondToAccessTokenRequest($serverRequest, $serverResponse);
} catch (OAuthServerException $e) {
- return $e->generateHttpResponse($serverResponse);
+ $event = $this->exceptionEventFactory->handleLeagueException($e);
+ return $event->getResponse();
}
}
}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 707b2266..547cb84e 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -27,10 +27,6 @@ public function getConfigTreeBuilder(): TreeBuilder
$rootNode
->children()
- ->scalarNode('exception_event_listener_priority')
- ->info('The priority of the event listener that converts an Exception to a Response.')
- ->defaultValue(10)
- ->end()
->scalarNode('role_prefix')
->info('Set a custom prefix that replaces the default "ROLE_OAUTH2_" role prefix.')
->defaultValue('ROLE_OAUTH2_')
diff --git a/DependencyInjection/TrikoderOAuth2Extension.php b/DependencyInjection/TrikoderOAuth2Extension.php
index 5690aada..3910f326 100644
--- a/DependencyInjection/TrikoderOAuth2Extension.php
+++ b/DependencyInjection/TrikoderOAuth2Extension.php
@@ -33,7 +33,7 @@
use Trikoder\Bundle\OAuth2Bundle\DBAL\Type\Grant as GrantType;
use Trikoder\Bundle\OAuth2Bundle\DBAL\Type\RedirectUri as RedirectUriType;
use Trikoder\Bundle\OAuth2Bundle\DBAL\Type\Scope as ScopeType;
-use Trikoder\Bundle\OAuth2Bundle\EventListener\ConvertExceptionToResponseListener;
+use Trikoder\Bundle\OAuth2Bundle\EventListener\ExceptionToOauthResponseListener;
use Trikoder\Bundle\OAuth2Bundle\League\AuthorizationServer\GrantTypeInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\Doctrine\AccessTokenManager;
use Trikoder\Bundle\OAuth2Bundle\Manager\Doctrine\AuthorizationCodeManager;
@@ -66,15 +66,15 @@ public function load(array $configs, ContainerBuilder $container)
$container->getDefinition(OAuth2TokenFactory::class)
->setArgument(0, $config['role_prefix']);
- $container->getDefinition(ConvertExceptionToResponseListener::class)
+ $container->registerForAutoconfiguration(GrantTypeInterface::class)
+ ->addTag('trikoder.oauth2.authorization_server.grant');
+
+ $container->getDefinition(ExceptionToOauthResponseListener::class)
->addTag('kernel.event_listener', [
'event' => KernelEvents::EXCEPTION,
'method' => 'onKernelException',
- 'priority' => $config['exception_event_listener_priority'],
+ 'priority' => 10
]);
-
- $container->registerForAutoconfiguration(GrantTypeInterface::class)
- ->addTag('trikoder.oauth2.authorization_server.grant');
}
/**
diff --git a/Event/OauthEvent/AbstractOauthEvent.php b/Event/OauthEvent/AbstractOauthEvent.php
new file mode 100644
index 00000000..de737755
--- /dev/null
+++ b/Event/OauthEvent/AbstractOauthEvent.php
@@ -0,0 +1,57 @@
+
+ */
+abstract class AbstractOauthEvent extends Event
+{
+ /**
+ * @var OAuthServerException
+ */
+ protected $exception;
+
+ /**
+ * @var ResponseInterface
+ */
+ protected $response;
+
+ public function __construct(OAuthServerException $exception, ResponseInterface $response)
+ {
+ $this->exception = $exception;
+ $this->response = $response;
+ }
+
+ /**
+ * @return string The event name that will be use with the eventDispatcher
+ */
+ abstract function getEventName(): string;
+
+ public function getException(): OAuthServerException
+ {
+ return $this->exception;
+ }
+
+ public function getResponse(): ResponseInterface
+ {
+ return $this->response;
+ }
+
+ /**
+ * @param ResponseInterface $response
+ * @return $this
+ */
+ public function setResponse(ResponseInterface $response): AbstractOauthEvent
+ {
+ $this->response = $response;
+ return $this;
+ }
+}
diff --git a/Event/OauthEvent/AuthenticationFailureEvent.php b/Event/OauthEvent/AuthenticationFailureEvent.php
new file mode 100644
index 00000000..619a5ba9
--- /dev/null
+++ b/Event/OauthEvent/AuthenticationFailureEvent.php
@@ -0,0 +1,18 @@
+
+ */
+class AuthenticationFailureEvent extends AbstractOauthEvent
+{
+ function getEventName(): string
+ {
+ return OAuth2Events::AUTHENTICATION_FAILURE;
+ }
+}
diff --git a/Event/OauthEvent/AuthenticationScopeFailureEvent.php b/Event/OauthEvent/AuthenticationScopeFailureEvent.php
new file mode 100644
index 00000000..51a77192
--- /dev/null
+++ b/Event/OauthEvent/AuthenticationScopeFailureEvent.php
@@ -0,0 +1,37 @@
+
+ */
+class AuthenticationScopeFailureEvent extends AbstractOauthEvent
+{
+ /**
+ * @var TokenInterface|null
+ */
+ private $token;
+
+ public function __construct(OAuthServerException $exception, ResponseInterface $response, ?TokenInterface $token = null)
+ {
+ parent::__construct($exception, $response);
+ $this->token = $token;
+ }
+
+ function getEventName(): string
+ {
+ return OAuth2Events::AUTHENTICATION_SCOPE_FAILURE;
+ }
+
+ public function getToken(): ?TokenInterface
+ {
+ return $this->token;
+ }
+}
diff --git a/Event/OauthEvent/AuthorizationServerErrorEvent.php b/Event/OauthEvent/AuthorizationServerErrorEvent.php
new file mode 100644
index 00000000..dff69591
--- /dev/null
+++ b/Event/OauthEvent/AuthorizationServerErrorEvent.php
@@ -0,0 +1,18 @@
+
+ */
+class AuthorizationServerErrorEvent extends AbstractOauthEvent
+{
+ function getEventName(): string
+ {
+ return OAuth2Events::AUTHORIZATION_SERVER_ERROR;
+ }
+}
diff --git a/Event/OauthEvent/InvalidCredentialsEvent.php b/Event/OauthEvent/InvalidCredentialsEvent.php
new file mode 100644
index 00000000..82d5d4e9
--- /dev/null
+++ b/Event/OauthEvent/InvalidCredentialsEvent.php
@@ -0,0 +1,18 @@
+
+ */
+class InvalidCredentialsEvent extends AbstractOauthEvent
+{
+ function getEventName(): string
+ {
+ return OAuth2Events::INVALID_CREDENTIALS;
+ }
+}
diff --git a/Event/OauthEvent/MissingAuthorizationHeaderEvent.php b/Event/OauthEvent/MissingAuthorizationHeaderEvent.php
new file mode 100644
index 00000000..3995b6cd
--- /dev/null
+++ b/Event/OauthEvent/MissingAuthorizationHeaderEvent.php
@@ -0,0 +1,18 @@
+
+ */
+class MissingAuthorizationHeaderEvent extends AbstractOauthEvent
+{
+ function getEventName(): string
+ {
+ return OAuth2Events::AUTHORIZATION_HEADER_FAILURE;
+ }
+}
diff --git a/EventListener/ConvertExceptionToResponseListener.php b/EventListener/ConvertExceptionToResponseListener.php
deleted file mode 100644
index b29fdffe..00000000
--- a/EventListener/ConvertExceptionToResponseListener.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
- */
-final class ConvertExceptionToResponseListener
-{
- public function onKernelException(ExceptionEvent $event): void
- {
- $exception = $event->getThrowable();
- if ($exception instanceof InsufficientScopesException || $exception instanceof Oauth2AuthenticationFailedException) {
- $event->setResponse(new Response($exception->getMessage(), $exception->getCode()));
- }
- }
-}
diff --git a/EventListener/ExceptionToOauthResponseListener.php b/EventListener/ExceptionToOauthResponseListener.php
new file mode 100644
index 00000000..7eb0a9e0
--- /dev/null
+++ b/EventListener/ExceptionToOauthResponseListener.php
@@ -0,0 +1,41 @@
+exceptionEventFactory = $exceptionEventFactory;
+ }
+
+ /**
+ * This method will catch and convert all OAuthServerException to a nice ErrorResponse
+ * This will also trigger the event system
+ *
+ * @param ExceptionEvent $event
+ */
+ public function onKernelException(ExceptionEvent $event): void
+ {
+ $exception = $event->getThrowable();
+ if ($exception instanceof OAuthServerException) {
+ $updatedEvent = $this->exceptionEventFactory->handleLeagueException($exception);
+
+ $httpFoundationFactory = new HttpFoundationFactory();
+ $event->setResponse($httpFoundationFactory->createResponse($updatedEvent->getResponse()));
+ }
+ }
+}
diff --git a/League/Repository/UserRepository.php b/League/Repository/UserRepository.php
index bdacb26c..18711187 100644
--- a/League/Repository/UserRepository.php
+++ b/League/Repository/UserRepository.php
@@ -5,6 +5,7 @@
namespace Trikoder\Bundle\OAuth2Bundle\League\Repository;
use League\OAuth2\Server\Entities\ClientEntityInterface;
+use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Trikoder\Bundle\OAuth2Bundle\Converter\UserConverterInterface;
@@ -64,7 +65,7 @@ public function getUserEntityByUserCredentials(
$user = $event->getUser();
if (null === $user) {
- return null;
+ throw OAuthServerException::invalidCredentials();
}
return $this->userConverter->toLeague($user);
diff --git a/OAuth2Events.php b/OAuth2Events.php
index 8894eb9d..b956a3f5 100644
--- a/OAuth2Events.php
+++ b/OAuth2Events.php
@@ -7,7 +7,7 @@
final class OAuth2Events
{
/**
- * The USER_RESOLVE event occurrs when the client requests a "password"
+ * The USER_RESOLVE event occurs when the client requests a "password"
* grant type from the authorization server.
*
* You should set a valid user here if applicable.
@@ -15,7 +15,12 @@ final class OAuth2Events
public const USER_RESOLVE = 'trikoder.oauth2.user_resolve';
/**
- * The SCOPE_RESOLVE event occurrs right before the user obtains their
+ * The INVALID_CREDENTIALS event occurs when no user was found (invalid credentials)
+ */
+ public const INVALID_CREDENTIALS = 'trikoder.oauth2.invalid_credentials';
+
+ /**
+ * The SCOPE_RESOLVE event occurs right before the user obtains their
* valid access token.
*
* You could alter the access token's scope here.
@@ -23,11 +28,40 @@ final class OAuth2Events
public const SCOPE_RESOLVE = 'trikoder.oauth2.scope_resolve';
/**
- * The AUTHORIZATION_REQUEST_RESOLVE event occurrs right before the system
+ * The AUTHORIZATION_REQUEST_RESOLVE event occurs right before the system
* complete the authorization request.
*
* You could approve or deny the authorization request, or set the uri where
* must be redirected to resolve the authorization request.
*/
public const AUTHORIZATION_REQUEST_RESOLVE = 'trikoder.oauth2.authorization_request_resolve';
+
+ /**
+ * The AUTHORIZATION_HEADER_FAILURE event occurs when the
+ * Authorization Bearer header was not found, or is wrong/malformed
+ *
+ * You can set a custom error message in the response body
+ */
+ public const AUTHORIZATION_HEADER_FAILURE = 'trikoder.oauth2.authorization_header_failure';
+
+ /**
+ * The AUTHENTICATION_FAILURE event occurs when the oauth token verification failed
+ *
+ * You can set a custom error message in the response body
+ */
+ public const AUTHENTICATION_FAILURE = 'trikoder.oauth2.authentication_failure';
+
+ /**
+ * The AUTHENTICATION_SCOPE_FAILURE event occurs when the scope validation for the token failed
+ *
+ * You can set a custom error message in the response body
+ */
+ public const AUTHENTICATION_SCOPE_FAILURE = 'trikoder.oauth2.authentication_scope_failure';
+
+ /**
+ * The AUTHORIZATION_SERVER_ERROR event occurs when the scope validation for the token failed
+ *
+ * You can set a custom error message in the response body
+ */
+ public const AUTHORIZATION_SERVER_ERROR = 'trikoder.oauth2.authorization_server_error';
}
diff --git a/README.md b/README.md
index 553a1c15..ba2fe258 100644
--- a/README.md
+++ b/README.md
@@ -206,6 +206,7 @@ security:
* [Controlling token scopes](docs/controlling-token-scopes.md)
* [Password grant handling](docs/password-grant-handling.md)
* [Implementing custom grant type](docs/implementing-custom-grant-type.md)
+* [Event/Data customization](docs/event-data-customization.md)
## Contributing
diff --git a/Resources/config/services.xml b/Resources/config/services.xml
index 00434642..1189b96a 100644
--- a/Resources/config/services.xml
+++ b/Resources/config/services.xml
@@ -84,6 +84,7 @@
+
@@ -99,6 +100,7 @@
+
@@ -153,6 +155,7 @@
+
@@ -168,14 +171,14 @@
The "%alias_id%" service alias is deprecated and will be removed in v4.
-
-
- The "%alias_id%" service alias is deprecated and will be removed in v4.
+
+
+
@@ -245,6 +248,10 @@
+
+
+
+
diff --git a/Security/Authentication/Provider/OAuth2Provider.php b/Security/Authentication/Provider/OAuth2Provider.php
index 3528b2e7..7030aa9d 100644
--- a/Security/Authentication/Provider/OAuth2Provider.php
+++ b/Security/Authentication/Provider/OAuth2Provider.php
@@ -58,13 +58,9 @@ public function authenticate(TokenInterface $token)
throw new RuntimeException(sprintf('This authentication provider can only handle tokes of type \'%s\'.', OAuth2Token::class));
}
- try {
- $request = $this->resourceServer->validateAuthenticatedRequest(
- $token->getAttribute('server_request')
- );
- } catch (OAuthServerException $e) {
- throw new AuthenticationException('The resource server rejected the request.', 0, $e);
- }
+ $request = $this->resourceServer->validateAuthenticatedRequest(
+ $token->getAttribute('server_request')
+ );
$user = $this->getAuthenticatedUser(
$request->getAttribute('oauth_user_id')
diff --git a/Security/Exception/ExceptionEventFactory.php b/Security/Exception/ExceptionEventFactory.php
new file mode 100644
index 00000000..1ba80d77
--- /dev/null
+++ b/Security/Exception/ExceptionEventFactory.php
@@ -0,0 +1,115 @@
+
+ */
+class ExceptionEventFactory
+{
+ protected const MAPPING_LEAGUE_EVENT = [
+ "invalid_client" => MissingAuthorizationHeaderEvent::class,
+ "invalid_scope" => AuthenticationScopeFailureEvent::class,
+ "invalid_credentials" => InvalidCredentialsEvent::class,
+ "server_error" => AuthorizationServerErrorEvent::class,
+ "access_denied" => AuthenticationFailureEvent::class,
+ ];
+
+ /**
+ * @var EventDispatcherInterface
+ */
+ private $eventDispatcher;
+
+ /**
+ * @var ResponseFactoryInterface
+ */
+ private $responseFactory;
+
+ public function __construct(EventDispatcherInterface $eventDispatcher, ResponseFactoryInterface $responseFactory)
+ {
+ $this->eventDispatcher = $eventDispatcher;
+ $this->responseFactory = $responseFactory;
+ }
+
+ private function generateResponse(OAuthServerException $exception): ResponseInterface
+ {
+ return $exception->generateHttpResponse($this->responseFactory->createResponse());
+ }
+
+ /**
+ * Will receive the league exception, find the right event to notify and the return it.
+ * Doing this give us the ability to notify other app and being able to apply their custom logic
+ *
+ * @param OAuthServerException $exception
+ */
+ public function handleLeagueException(OAuthServerException $exception): AbstractOauthEvent
+ {
+ if (array_key_exists($exception->getErrorType(), self::MAPPING_LEAGUE_EVENT)) {
+ $eventClass = self::MAPPING_LEAGUE_EVENT[$exception->getErrorType()];
+ /** @var AbstractOauthEvent $event */
+ $event = new $eventClass($exception, $this->generateResponse($exception));
+ $this->eventDispatcher->dispatch($event, $event->getEventName());
+ return $event;
+ } else { // We fallback to a generic event
+ $event = new AuthorizationServerErrorEvent($exception, $this->generateResponse($exception));
+ $this->eventDispatcher->dispatch($event, $event->getEventName());
+ return $event;
+ }
+ }
+
+ public function invalidClient(ServerRequestInterface $serverRequest): MissingAuthorizationHeaderEvent
+ {
+ $exception = OAuthServerException::invalidClient($serverRequest);
+
+ $event = new MissingAuthorizationHeaderEvent($exception, $this->generateResponse($exception));
+ $this->eventDispatcher->dispatch($event, $event->getEventName());
+
+ return $event;
+ }
+
+ public function invalidCredentials(): InvalidCredentialsEvent
+ {
+ $exception = OAuthServerException::invalidCredentials();
+
+ $event = new InvalidCredentialsEvent($exception, $this->generateResponse($exception));
+ $this->eventDispatcher->dispatch($event, $event->getEventName());
+
+ return $event;
+ }
+
+ public function accessDenied(Throwable $previous = null): AuthenticationFailureEvent
+ {
+ $exception = OAuthServerException::accessDenied(null, null, $previous);
+
+ $event = new AuthenticationFailureEvent($exception, $this->generateResponse($exception));
+ $this->eventDispatcher->dispatch($event, $event->getEventName());
+
+ return $event;
+ }
+
+ public function invalidScope(OAuth2Token $authenticatedToken, string $scope = ""): AuthenticationScopeFailureEvent
+ {
+ $exception = OAuthServerException::invalidScope($scope);
+
+ $event = new AuthenticationScopeFailureEvent($exception, $this->generateResponse($exception), $authenticatedToken);
+ $this->eventDispatcher->dispatch($event, $event->getEventName());
+
+ return $event;
+ }
+}
diff --git a/Security/Exception/InsufficientScopesException.php b/Security/Exception/InsufficientScopesException.php
deleted file mode 100644
index 74b14103..00000000
--- a/Security/Exception/InsufficientScopesException.php
+++ /dev/null
@@ -1,22 +0,0 @@
-
- */
-class InsufficientScopesException extends AuthenticationException
-{
- public static function create(TokenInterface $token): self
- {
- $exception = new self('The token has insufficient scopes.', 403);
- $exception->setToken($token);
-
- return $exception;
- }
-}
diff --git a/Security/Exception/Oauth2AuthenticationFailedException.php b/Security/Exception/Oauth2AuthenticationFailedException.php
deleted file mode 100644
index f42097ed..00000000
--- a/Security/Exception/Oauth2AuthenticationFailedException.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-class Oauth2AuthenticationFailedException extends AuthenticationException
-{
- public static function create(string $message): self
- {
- return new self($message, 401);
- }
-}
diff --git a/Security/Firewall/OAuth2Listener.php b/Security/Firewall/OAuth2Listener.php
index 36330fc7..52005b46 100644
--- a/Security/Firewall/OAuth2Listener.php
+++ b/Security/Firewall/OAuth2Listener.php
@@ -4,6 +4,7 @@
namespace Trikoder\Bundle\OAuth2Bundle\Security\Firewall;
+use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
@@ -12,8 +13,7 @@
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Trikoder\Bundle\OAuth2Bundle\Security\Authentication\Token\OAuth2Token;
use Trikoder\Bundle\OAuth2Bundle\Security\Authentication\Token\OAuth2TokenFactory;
-use Trikoder\Bundle\OAuth2Bundle\Security\Exception\InsufficientScopesException;
-use Trikoder\Bundle\OAuth2Bundle\Security\Exception\Oauth2AuthenticationFailedException;
+use Trikoder\Bundle\OAuth2Bundle\Security\Exception\ExceptionEventFactory;
final class OAuth2Listener
{
@@ -37,6 +37,11 @@ final class OAuth2Listener
*/
private $oauth2TokenFactory;
+ /**
+ * @var ExceptionEventFactory
+ */
+ private $exceptionEventFactory;
+
/**
* @var string
*/
@@ -46,12 +51,14 @@ public function __construct(
TokenStorageInterface $tokenStorage,
AuthenticationManagerInterface $authenticationManager,
HttpMessageFactoryInterface $httpMessageFactory,
+ ExceptionEventFactory $exceptionEventFactory,
OAuth2TokenFactory $oauth2TokenFactory,
string $providerKey
) {
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
$this->httpMessageFactory = $httpMessageFactory;
+ $this->exceptionEventFactory = $exceptionEventFactory;
$this->oauth2TokenFactory = $oauth2TokenFactory;
$this->providerKey = $providerKey;
}
@@ -59,8 +66,11 @@ public function __construct(
public function __invoke(RequestEvent $event)
{
$request = $this->httpMessageFactory->createRequest($event->getRequest());
+ $responseFactory = new HttpFoundationFactory();
if (!$request->hasHeader('Authorization')) {
+ $missingAuthHeaderEvent = $this->exceptionEventFactory->invalidClient($request);
+ $event->setResponse($responseFactory->createResponse($missingAuthHeaderEvent->getResponse()));
return;
}
@@ -68,11 +78,15 @@ public function __invoke(RequestEvent $event)
/** @var OAuth2Token $authenticatedToken */
$authenticatedToken = $this->authenticationManager->authenticate($this->oauth2TokenFactory->createOAuth2Token($request, null, $this->providerKey));
} catch (AuthenticationException $e) {
- throw new Oauth2AuthenticationFailedException($e->getMessage(), 401, $e);
+ $authenticationFailureEvent = $this->exceptionEventFactory->accessDenied($e);
+ $event->setResponse($responseFactory->createResponse($authenticationFailureEvent->getResponse()));
+ return;
}
if (!$this->isAccessToRouteGranted($event->getRequest(), $authenticatedToken)) {
- throw InsufficientScopesException::create($authenticatedToken);
+ $authenticationFailureScopeEvent = $this->exceptionEventFactory->invalidScope($authenticatedToken);
+ $event->setResponse($responseFactory->createResponse($authenticationFailureScopeEvent->getResponse()));
+ return;
}
$this->tokenStorage->setToken($authenticatedToken);
diff --git a/Security/Guard/Authenticator/OAuth2Authenticator.php b/Security/Guard/Authenticator/OAuth2Authenticator.php
index 4fd87a81..65141901 100644
--- a/Security/Guard/Authenticator/OAuth2Authenticator.php
+++ b/Security/Guard/Authenticator/OAuth2Authenticator.php
@@ -6,43 +6,53 @@
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
+use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AuthenticatorInterface;
+use Trikoder\Bundle\OAuth2Bundle\Response\ErrorJsonResponse;
use Trikoder\Bundle\OAuth2Bundle\Security\Authentication\Token\OAuth2Token;
use Trikoder\Bundle\OAuth2Bundle\Security\Authentication\Token\OAuth2TokenFactory;
-use Trikoder\Bundle\OAuth2Bundle\Security\Exception\InsufficientScopesException;
+use Trikoder\Bundle\OAuth2Bundle\Security\Exception\ExceptionEventFactory;
use Trikoder\Bundle\OAuth2Bundle\Security\User\NullUser;
/**
* @author Yonel Ceruto
* @author Antonio J. GarcĂa Lagar
+ * @author Benoit VIGNAL
*/
final class OAuth2Authenticator implements AuthenticatorInterface
{
private $httpMessageFactory;
private $resourceServer;
private $oauth2TokenFactory;
+ /** @var HttpMessageFactoryInterface */
private $psr7Request;
- public function __construct(HttpMessageFactoryInterface $httpMessageFactory, ResourceServer $resourceServer, OAuth2TokenFactory $oauth2TokenFactory)
+ /**
+ * @var ExceptionEventFactory
+ */
+ private $exceptionEventFactory;
+
+ public function __construct(HttpMessageFactoryInterface $httpMessageFactory, ResourceServer $resourceServer, OAuth2TokenFactory $oauth2TokenFactory, ExceptionEventFactory $exceptionEventFactory)
{
$this->httpMessageFactory = $httpMessageFactory;
$this->resourceServer = $resourceServer;
$this->oauth2TokenFactory = $oauth2TokenFactory;
+ $this->exceptionEventFactory = $exceptionEventFactory;
}
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
- $exception = new UnauthorizedHttpException('Bearer');
-
- return new Response('', $exception->getStatusCode(), $exception->getHeaders());
+ $request = $this->httpMessageFactory->createRequest($request);
+ $missingAuthHeaderEvent = $this->exceptionEventFactory->invalidClient($request);
+ $httpFoundationFactory = new HttpFoundationFactory();
+ return $httpFoundationFactory->createResponse($missingAuthHeaderEvent->getResponse());
}
public function supports(Request $request): bool
@@ -54,11 +64,8 @@ public function getCredentials(Request $request)
{
$psr7Request = $this->httpMessageFactory->createRequest($request);
- try {
- $this->psr7Request = $this->resourceServer->validateAuthenticatedRequest($psr7Request);
- } catch (OAuthServerException $e) {
- throw new AuthenticationException('The resource server rejected the request.', 0, $e);
- }
+ // Error will be automatically catch and converted in ExceptionToOauthResponseListener
+ $this->psr7Request = $this->resourceServer->validateAuthenticatedRequest($psr7Request);
return $this->psr7Request->getAttribute('oauth_user_id');
}
@@ -80,7 +87,8 @@ public function createAuthenticatedToken(UserInterface $user, $providerKey): OAu
$oauth2Token = $this->oauth2TokenFactory->createOAuth2Token($this->psr7Request, $tokenUser, $providerKey);
if (!$this->isAccessToRouteGranted($oauth2Token)) {
- throw InsufficientScopesException::create($oauth2Token);
+ // In the hint the route scope will be showed
+ throw OAuthServerException::invalidScope($this->getRouteScopes());
}
$oauth2Token->setAuthenticated(true);
@@ -92,7 +100,14 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio
{
$this->psr7Request = null;
- throw $exception;
+ if ($exception instanceof OAuthServerException) {
+ $event = $this->exceptionEventFactory->handleLeagueException($exception);
+ } else {
+ $event = $this->exceptionEventFactory->accessDenied($exception);
+ }
+
+ $httpFoundationFactory = new HttpFoundationFactory();
+ return $httpFoundationFactory->createResponse($event->getResponse());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
@@ -105,9 +120,14 @@ public function supportsRememberMe(): bool
return false;
}
+ private function getRouteScopes(): array
+ {
+ return $this->psr7Request->getAttribute('oauth2_scopes', []);
+ }
+
private function isAccessToRouteGranted(OAuth2Token $token): bool
{
- $routeScopes = $this->psr7Request->getAttribute('oauth2_scopes', []);
+ $routeScopes = $this->getRouteScopes();
if ([] === $routeScopes) {
return true;
diff --git a/Tests/Integration/AuthorizationServerTest.php b/Tests/Integration/AuthorizationServerTest.php
index cc1e9f40..28753f2f 100644
--- a/Tests/Integration/AuthorizationServerTest.php
+++ b/Tests/Integration/AuthorizationServerTest.php
@@ -339,8 +339,8 @@ public function testInvalidCredentialsPasswordGrant(): void
$response = $this->handleTokenRequest($request);
// Response assertions.
- $this->assertSame('invalid_grant', $response['error']);
- $this->assertSame('The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.', $response['message']);
+ $this->assertSame('invalid_credentials', $response['error']);
+ $this->assertSame('The user credentials were incorrect.', $response['message']);
}
public function testMissingUsernameFieldPasswordGrant(): void
diff --git a/UPGRADE.md b/UPGRADE.md
index 5ac62f79..97cf3e63 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -1,6 +1,24 @@
# Upgrade
Here you will find upgrade steps between releases.
+## From 3.2.0 to 4.0.0
+
+### Remove `exception_event_listener_priority`
+The bundle configuration `exception_event_listener_priority` has been removed in favor of [Event](docs/event-data-customization.md).
+
+### Error response are now formatted in json by default
+All the error response are now formatted as json (no formatting before).
+
+They can be modified using the new event system.
+
+All the errors follow the same structure :
+```json
+{
+ "message": ""
+}
+```
+
+
## From 3.1.1 to 3.2.0
The bundle makes modifications to the existing schema. You will need to run the Doctrine schema update process to sync the changes:
diff --git a/docs/event-data-customization.md b/docs/event-data-customization.md
new file mode 100644
index 00000000..f67f336c
--- /dev/null
+++ b/docs/event-data-customization.md
@@ -0,0 +1,117 @@
+# Event/Data customization
+
+## Table of contents
+- [AUTHORIZATION_HEADER_FAILURE - Customizing the response on invalid authorization header](#oauth2eventsmissing_authorization_header---customizing-the-response-on-invalid-authorization-header)
+- [AUTHENTICATION_SCOPE_FAILURE - Customizing the response on invalid scope](#oauth2eventsauthentication_scope_failure---customizing-the-response-on-invalid-scope)
+- [INVALID_CREDENTIALS - Customizing the response on credentials failure](#oauth2eventsinvalid_credentials---customizing-the-response-on-credentials-failure)
+- [AUTHENTICATION_FAILURE - Customizing the response on authentication failure](#oauth2eventsauthentication_failure---customizing-the-response-on-authentication-failure)
+
+## OAuth2Events::AUTHORIZATION_HEADER_FAILURE - Customizing the response on invalid authorization header
+
+Called when the `Authorization Bearer` was not found or is malformed.
+
+Example:
+
+```php
+ "onMissingAuthorizationHeader",
+ ];
+ }
+
+ public function onMissingAuthorizationHeader(MissingAuthorizationHeaderEvent $event): void {
+ $response = new JsonResponse("Invalid header.", Response::HTTP_UNAUTHORIZED);
+ $event->setResponse($response);
+ }
+}
+```
+
+## OAuth2Events::AUTHENTICATION_SCOPE_FAILURE - Customizing the response on invalid scope
+
+Called when the user didn't have the right scope defined.
+
+Example:
+
+```php
+ "onInvalidScope",
+ ];
+ }
+
+ public function onInvalidScope(AuthenticationScopeFailureEvent $event): void {
+ $response = new JsonResponse("Invalid scope.", Response::HTTP_UNAUTHORIZED);
+ $event->setResponse($response);
+ }
+}
+```
+
+## OAuth2Events::INVALID_CREDENTIALS - Customizing the response on credentials failure
+
+Called when the credential verification failed.
+
+Often when no user was return after calling `OAuth2Events::USER_RESOLVE`
+
+Example:
+
+```php
+ "onInvalidCredentials",
+ ];
+ }
+
+ public function onInvalidCredentials(InvalidCredentialsEvent $event): void {
+ $response = new JsonResponse("Wrong username/password.", Response::HTTP_UNAUTHORIZED);
+ $event->setResponse($response);
+ }
+}
+```
+
+## OAuth2Events::AUTHENTICATION_FAILURE - Customizing the response on authentication failure
+
+Called when the authentication failed.
+
+Example:
+
+```php
+ "onAuthenticationFailure",
+ ];
+ }
+
+ public function onAuthenticationFailure(AuthenticationFailureEvent $event): void {
+ $response = new JsonResponse("Invalid scope.", Response::HTTP_UNAUTHORIZED);
+ $event->setResponse($response);
+ }
+}
+```
diff --git a/docs/password-grant-handling.md b/docs/password-grant-handling.md
index 5c58f88d..617228b0 100644
--- a/docs/password-grant-handling.md
+++ b/docs/password-grant-handling.md
@@ -2,6 +2,9 @@
The `password` grant issues access and refresh tokens that are bound to both a client and a user within your application. As user system implementations can differ greatly on an application basis, the `trikoder.oauth2.user_resolve` was created which allows you to decide which user you want to bind to issuing tokens.
+After triggering this event, if no user was returned an [`OAuth2Events::INVALID_CREDENTIALS`](event-data-customization.md#oauth2eventsinvalid_credentials---customizing-the-response-on-credentials-failure) will be triggered.
+Within this new event you'll be able to define a custom error response for example.
+
## Requirements
The user model should implement the `Symfony\Component\Security\Core\User\UserInterface` interface.