diff --git a/Controller/AuthorizationController.php b/Controller/AuthorizationController.php
new file mode 100644
index 00000000..20f37310
--- /dev/null
+++ b/Controller/AuthorizationController.php
@@ -0,0 +1,80 @@
+server = $server;
+ $this->security = $security;
+ $this->eventDispatcher = $eventDispatcher;
+ }
+
+ public function indexAction(ServerRequestInterface $serverRequest, ResponseFactoryInterface $responseFactory): ResponseInterface
+ {
+ $serverResponse = $responseFactory->createResponse();
+
+ try {
+ $authRequest = $this->server->validateAuthorizationRequest($serverRequest);
+ $authRequest->setUser($this->getUserEntity());
+
+ /** @var AuthorizationRequestResolveEvent $event */
+ $event = $this->eventDispatcher->dispatch(
+ OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE,
+ new AuthorizationRequestResolveEvent($authRequest)
+ );
+
+ if ($event->hasResponse()) {
+ return $event->getResponse();
+ }
+
+ $authRequest->setAuthorizationApproved($event->getAuthorizationResolution());
+
+ return $this->server->completeAuthorizationRequest($authRequest, $serverResponse);
+ } catch (OAuthServerException $e) {
+ return $e->generateHttpResponse($serverResponse);
+ }
+ }
+
+ private function getUserEntity(): User
+ {
+ $userEntity = new User();
+
+ $user = $this->security->getUser();
+ if ($user instanceof UserInterface) {
+ $userEntity->setIdentifier($user->getUsername());
+ }
+
+ return $userEntity;
+ }
+}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 8d5f0de2..46b5cfaa 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -59,6 +59,11 @@ private function createAuthorizationServerNode(): NodeDefinition
->cannotBeEmpty()
->defaultValue('P1M')
->end()
+ ->scalarNode('auth_code_ttl')
+ ->info("How long the issued auth code should be valid for.\nThe value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters")
+ ->cannotBeEmpty()
+ ->defaultValue('PT10M')
+ ->end()
->booleanNode('enable_client_credentials_grant')
->info('Whether to enable the client credentials grant')
->defaultTrue()
@@ -71,6 +76,10 @@ private function createAuthorizationServerNode(): NodeDefinition
->info('Whether to enable the refresh token grant')
->defaultTrue()
->end()
+ ->booleanNode('enable_auth_code_grant')
+ ->info('Whether to enable the authorization code grant')
+ ->defaultTrue()
+ ->end()
->end()
;
diff --git a/DependencyInjection/TrikoderOAuth2Extension.php b/DependencyInjection/TrikoderOAuth2Extension.php
index 27e772b4..1f1f59c7 100644
--- a/DependencyInjection/TrikoderOAuth2Extension.php
+++ b/DependencyInjection/TrikoderOAuth2Extension.php
@@ -164,6 +164,13 @@ private function configureAuthorizationServer(ContainerBuilder $container, array
]);
}
+ if ($config['enable_auth_code_grant']) {
+ $authorizationServer->addMethodCall('enableGrantType', [
+ new Reference('league.oauth2.server.grant.auth_code_grant'),
+ new Definition(DateInterval::class, [$config['access_token_ttl']]),
+ ]);
+ }
+
$this->configureGrants($container, $config);
}
@@ -182,6 +189,14 @@ private function configureGrants(ContainerBuilder $container, array $config): vo
new Definition(DateInterval::class, [$config['refresh_token_ttl']]),
])
;
+
+ $container
+ ->getDefinition('league.oauth2.server.grant.auth_code_grant')
+ ->replaceArgument('$authCodeTTL', new Definition(DateInterval::class, [$config['auth_code_ttl']]))
+ ->addMethodCall('setRefreshTokenTTL', [
+ new Definition(DateInterval::class, [$config['refresh_token_ttl']]),
+ ])
+ ;
}
private function configurePersistence(LoaderInterface $loader, ContainerBuilder $container, array $config)
@@ -228,6 +243,11 @@ private function configureDoctrinePersistence(ContainerBuilder $container, array
->replaceArgument('$entityManager', $entityManager)
;
+ $container
+ ->getDefinition('trikoder.oauth2.manager.doctrine.authorization_code_manager')
+ ->replaceArgument('$entityManager', $entityManager)
+ ;
+
$container->setParameter('trikoder.oauth2.persistence.doctrine.enabled', true);
$container->setParameter('trikoder.oauth2.persistence.doctrine.manager', $entityManagerName);
}
diff --git a/Event/AuthorizationRequestResolveEvent.php b/Event/AuthorizationRequestResolveEvent.php
new file mode 100644
index 00000000..2ad51551
--- /dev/null
+++ b/Event/AuthorizationRequestResolveEvent.php
@@ -0,0 +1,117 @@
+authorizationRequest = $authorizationRequest;
+ }
+
+ public function getAuthorizationResolution(): bool
+ {
+ return $this->authorizationResolution;
+ }
+
+ public function resolveAuthorization(bool $authorizationResolution): void
+ {
+ $this->authorizationResolution = $authorizationResolution;
+ $this->response = null;
+ }
+
+ public function hasResponse(): bool
+ {
+ return $this->response instanceof ResponseInterface;
+ }
+
+ public function getResponse(): ResponseInterface
+ {
+ if (!$this->hasResponse()) {
+ throw new LogicException('There is no response. You should call "hasResponse" to check if the response exists.');
+ }
+
+ return $this->response;
+ }
+
+ public function setResponse(ResponseInterface $response): void
+ {
+ $this->response = $response;
+ }
+
+ public function getGrantTypeId(): string
+ {
+ return $this->authorizationRequest->getGrantTypeId();
+ }
+
+ public function getClient(): ClientEntityInterface
+ {
+ return $this->authorizationRequest->getClient();
+ }
+
+ public function getUser(): UserEntityInterface
+ {
+ return $this->authorizationRequest->getUser();
+ }
+
+ /**
+ * @return ScopeEntityInterface[]
+ */
+ public function getScopes(): array
+ {
+ return $this->authorizationRequest->getScopes();
+ }
+
+ public function isAuthorizationApproved(): bool
+ {
+ return $this->authorizationRequest->isAuthorizationApproved();
+ }
+
+ public function getRedirectUri(): ?string
+ {
+ return $this->authorizationRequest->getRedirectUri();
+ }
+
+ public function getState(): ?string
+ {
+ return $this->authorizationRequest->getState();
+ }
+
+ public function getCodeChallenge(): string
+ {
+ return $this->authorizationRequest->getCodeChallenge();
+ }
+
+ public function getCodeChallengeMethod(): string
+ {
+ return $this->authorizationRequest->getCodeChallengeMethod();
+ }
+}
diff --git a/League/Entity/AuthCode.php b/League/Entity/AuthCode.php
new file mode 100644
index 00000000..7af21cdc
--- /dev/null
+++ b/League/Entity/AuthCode.php
@@ -0,0 +1,17 @@
+authorizationCodeManager = $authorizationCodeManager;
+ $this->clientManager = $clientManager;
+ $this->scopeConverter = $scopeConverter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNewAuthCode()
+ {
+ return new AuthCode();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function persistNewAuthCode(AuthCodeEntityInterface $authCode)
+ {
+ $authorizationCode = $this->authorizationCodeManager->find($authCode->getIdentifier());
+
+ if (null !== $authorizationCode) {
+ throw UniqueTokenIdentifierConstraintViolationException::create();
+ }
+
+ $authorizationCode = $this->buildAuthorizationCode($authCode);
+
+ $this->authorizationCodeManager->save($authorizationCode);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function revokeAuthCode($codeId)
+ {
+ $authorizationCode = $this->authorizationCodeManager->find($codeId);
+
+ if (null === $codeId) {
+ return;
+ }
+
+ $authorizationCode->revoke();
+
+ $this->authorizationCodeManager->save($authorizationCode);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isAuthCodeRevoked($codeId)
+ {
+ $authorizationCode = $this->authorizationCodeManager->find($codeId);
+
+ if (null === $authorizationCode) {
+ return true;
+ }
+
+ return $authorizationCode->isRevoked();
+ }
+
+ private function buildAuthorizationCode(AuthCode $authCode): AuthorizationCode
+ {
+ $client = $this->clientManager->find($authCode->getClient()->getIdentifier());
+
+ $authorizationCode = new AuthorizationCode(
+ $authCode->getIdentifier(),
+ $authCode->getExpiryDateTime(),
+ $client,
+ $authCode->getUserIdentifier(),
+ $this->scopeConverter->toDomainArray($authCode->getScopes())
+ );
+
+ return $authorizationCode;
+ }
+}
diff --git a/Manager/AuthorizationCodeManagerInterface.php b/Manager/AuthorizationCodeManagerInterface.php
new file mode 100644
index 00000000..4775c9d9
--- /dev/null
+++ b/Manager/AuthorizationCodeManagerInterface.php
@@ -0,0 +1,14 @@
+entityManager = $entityManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function find(string $identifier): ?AuthorizationCode
+ {
+ return $this->entityManager->find(AuthorizationCode::class, $identifier);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(AuthorizationCode $authorizationCode): void
+ {
+ $this->entityManager->persist($authorizationCode);
+ $this->entityManager->flush();
+ }
+}
diff --git a/Manager/InMemory/AuthorizationCodeManager.php b/Manager/InMemory/AuthorizationCodeManager.php
new file mode 100644
index 00000000..a51f36d1
--- /dev/null
+++ b/Manager/InMemory/AuthorizationCodeManager.php
@@ -0,0 +1,26 @@
+authorizationCodes[$identifier] ?? null;
+ }
+
+ public function save(AuthorizationCode $authorizationCode): void
+ {
+ $this->authorizationCodes[$authorizationCode->getIdentifier()] = $authorizationCode;
+ }
+}
diff --git a/Model/AuthorizationCode.php b/Model/AuthorizationCode.php
new file mode 100644
index 00000000..5300c551
--- /dev/null
+++ b/Model/AuthorizationCode.php
@@ -0,0 +1,99 @@
+identifier = $identifier;
+ $this->expiry = $expiry;
+ $this->client = $client;
+ $this->userIdentifier = $userIdentifier;
+ $this->scopes = $scopes;
+ }
+
+ public function __toString(): string
+ {
+ return $this->getIdentifier();
+ }
+
+ public function getIdentifier(): string
+ {
+ return $this->identifier;
+ }
+
+ public function getExpiryDateTime(): DateTime
+ {
+ return $this->expiry;
+ }
+
+ public function getUserIdentifier(): ?string
+ {
+ return $this->userIdentifier;
+ }
+
+ public function getClient(): Client
+ {
+ return $this->client;
+ }
+
+ /**
+ * @return Scope[]
+ */
+ public function getScopes(): array
+ {
+ return $this->scopes;
+ }
+
+ public function isRevoked(): bool
+ {
+ return $this->revoked;
+ }
+
+ public function revoke(): self
+ {
+ $this->revoked = true;
+
+ return $this;
+ }
+}
diff --git a/OAuth2Events.php b/OAuth2Events.php
index e6334793..8894eb9d 100644
--- a/OAuth2Events.php
+++ b/OAuth2Events.php
@@ -21,4 +21,13 @@ final class OAuth2Events
* You could alter the access token's scope here.
*/
public const SCOPE_RESOLVE = 'trikoder.oauth2.scope_resolve';
+
+ /**
+ * The AUTHORIZATION_REQUEST_RESOLVE event occurrs 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';
}
diff --git a/OAuth2Grants.php b/OAuth2Grants.php
index 08cc063e..5b2f54cd 100644
--- a/OAuth2Grants.php
+++ b/OAuth2Grants.php
@@ -43,11 +43,12 @@ final class OAuth2Grants
public static function has(string $grant): bool
{
- // TODO: Add support for "authorization_code" and "implicit" grant types.
+ // TODO: Add support for "implicit" grant type.
return \in_array($grant, [
self::CLIENT_CREDENTIALS,
self::PASSWORD,
self::REFRESH_TOKEN,
+ self::AUTHORIZATION_CODE,
]);
}
}
diff --git a/README.md b/README.md
index f8144efc..ea7c7925 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,10 @@ This package is currently in the active development.
# Whether to enable the refresh token grant
enable_refresh_token_grant: true
+ # How long the issued auth code should be valid for.
+ # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters
+ auth_code_ttl: PT10M
+
resource_server:
# Full path to the public key file
@@ -118,6 +122,16 @@ This package is currently in the active development.
You can verify that everything is working by issuing a `GET` request to the `/token` endpoint.
+**❮ NOTE ❯** It is recommended to control the access to the authorization endpoint
+so that only logged in users can approve authorization requests.
+You should review your `security.yml` file. Here is a sample configuration:
+
+```yaml
+security:
+ access_control:
+ - { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED }
+```
+
## Configuration
* [Basic setup](docs/basic-setup.md)
diff --git a/Resources/config/doctrine/model/AuthorizationCode.orm.xml b/Resources/config/doctrine/model/AuthorizationCode.orm.xml
new file mode 100644
index 00000000..1beaa55b
--- /dev/null
+++ b/Resources/config/doctrine/model/AuthorizationCode.orm.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/config/routes.xml b/Resources/config/routes.xml
index 07a7f883..fc7df6c8 100644
--- a/Resources/config/routes.xml
+++ b/Resources/config/routes.xml
@@ -3,5 +3,6 @@
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
+
diff --git a/Resources/config/services.xml b/Resources/config/services.xml
index 1a41500e..5ce84f77 100644
--- a/Resources/config/services.xml
+++ b/Resources/config/services.xml
@@ -29,6 +29,11 @@
+
+
+
+
+
@@ -66,6 +71,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/config/storage/doctrine.xml b/Resources/config/storage/doctrine.xml
index 780a23fe..a411da0d 100644
--- a/Resources/config/storage/doctrine.xml
+++ b/Resources/config/storage/doctrine.xml
@@ -7,6 +7,7 @@
+
@@ -19,5 +20,8 @@
+
+
+
diff --git a/Resources/config/storage/in_memory.xml b/Resources/config/storage/in_memory.xml
index 9b9f4199..1e3ceb29 100644
--- a/Resources/config/storage/in_memory.xml
+++ b/Resources/config/storage/in_memory.xml
@@ -7,11 +7,13 @@
+
+
diff --git a/Tests/Acceptance/AuthorizationEndpointTest.php b/Tests/Acceptance/AuthorizationEndpointTest.php
new file mode 100644
index 00000000..fff4ac0b
--- /dev/null
+++ b/Tests/Acceptance/AuthorizationEndpointTest.php
@@ -0,0 +1,155 @@
+client->getContainer()->get(ScopeManagerInterface::class),
+ $this->client->getContainer()->get(ClientManagerInterface::class),
+ $this->client->getContainer()->get(AccessTokenManagerInterface::class),
+ $this->client->getContainer()->get(RefreshTokenManagerInterface::class),
+ $this->client->getContainer()->get(AuthorizationCodeManagerInterface::class)
+ );
+ }
+
+ public function testSuccessfulCodeRequest()
+ {
+ $this->client
+ ->getContainer()
+ ->get('event_dispatcher')
+ ->addListener(OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE, function (AuthorizationRequestResolveEvent $event) {
+ $event->resolveAuthorization(AuthorizationRequestResolveEvent::AUTHORIZATION_APPROVED);
+ });
+
+ timecop_freeze(new DateTime());
+
+ $this->client->request(
+ 'GET',
+ '/authorize',
+ [
+ 'client_id' => FixtureFactory::FIXTURE_CLIENT_FIRST,
+ 'response_type' => 'code',
+ 'state' => 'foobar',
+ ]
+ );
+
+ timecop_return();
+
+ $response = $this->client->getResponse();
+
+ $this->assertSame(302, $response->getStatusCode());
+ $redirectUri = $response->headers->get('Location');
+
+ $this->assertStringStartsWith(FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI, $redirectUri);
+ $query = [];
+ parse_str(parse_url($redirectUri, PHP_URL_QUERY), $query);
+ $this->assertArrayHasKey('code', $query);
+ $this->assertArrayHasKey('state', $query);
+ $this->assertEquals('foobar', $query['state']);
+ }
+
+ public function testCodeRequestRedirectToResolutionUri()
+ {
+ $this->client
+ ->getContainer()
+ ->get('event_dispatcher')
+ ->addListener(OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE, function (AuthorizationRequestResolveEvent $event) {
+ $response = (new Response())->withStatus(302)->withHeader('Location', '/authorize/consent');
+ $event->setResponse($response);
+ });
+
+ timecop_freeze(new DateTime());
+
+ $this->client->request(
+ 'GET',
+ '/authorize',
+ [
+ 'client_id' => FixtureFactory::FIXTURE_CLIENT_FIRST,
+ 'response_type' => 'code',
+ 'state' => 'foobar',
+ 'redirect_uri' => FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI,
+ 'scope' => FixtureFactory::FIXTURE_SCOPE_FIRST . ' ' . FixtureFactory::FIXTURE_SCOPE_SECOND,
+ ]
+ );
+
+ timecop_return();
+
+ $response = $this->client->getResponse();
+
+ $this->assertSame(302, $response->getStatusCode());
+ $redirectUri = $response->headers->get('Location');
+ $this->assertEquals('/authorize/consent', $redirectUri);
+ }
+
+ public function testFailedCodeRequestRedirectWithFakedRedirectUri()
+ {
+ $this->client
+ ->getContainer()
+ ->get('event_dispatcher')
+ ->addListener(OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE, function (AuthorizationRequestResolveEvent $event) {
+ $event->resolveAuthorization(AuthorizationRequestResolveEvent::AUTHORIZATION_APPROVED);
+ });
+
+ timecop_freeze(new DateTime());
+
+ $this->client->request(
+ 'GET',
+ '/authorize',
+ [
+ 'client_id' => FixtureFactory::FIXTURE_CLIENT_FIRST,
+ 'response_type' => 'code',
+ 'state' => 'foobar',
+ 'redirect_uri' => 'https://example.org/oauth2/malicious-uri',
+ ]
+ );
+
+ timecop_return();
+
+ $response = $this->client->getResponse();
+
+ $this->assertSame(401, $response->getStatusCode());
+ $this->assertSame('application/json', $response->headers->get('Content-Type'));
+
+ $jsonResponse = json_decode($response->getContent(), true);
+
+ $this->assertSame('invalid_client', $jsonResponse['error']);
+ $this->assertSame('Client authentication failed', $jsonResponse['message']);
+ }
+
+ public function testFailedAuthorizeRequest()
+ {
+ $this->client->request(
+ 'GET',
+ '/authorize'
+ );
+
+ $response = $this->client->getResponse();
+
+ $this->assertSame(400, $response->getStatusCode());
+ $this->assertSame('application/json', $response->headers->get('Content-Type'));
+
+ $jsonResponse = json_decode($response->getContent(), true);
+
+ $this->assertSame('unsupported_grant_type', $jsonResponse['error']);
+ $this->assertSame('The authorization grant type is not supported by the authorization server.', $jsonResponse['message']);
+ $this->assertSame('Check that all required parameters have been provided', $jsonResponse['hint']);
+ }
+}
diff --git a/Tests/Acceptance/ClearExpiredTokensCommandTest.php b/Tests/Acceptance/ClearExpiredTokensCommandTest.php
index 13f51fc9..abdd198a 100644
--- a/Tests/Acceptance/ClearExpiredTokensCommandTest.php
+++ b/Tests/Acceptance/ClearExpiredTokensCommandTest.php
@@ -8,6 +8,7 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
+use Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
@@ -25,7 +26,8 @@ protected function setUp(): void
$this->client->getContainer()->get(ScopeManagerInterface::class),
$this->client->getContainer()->get(ClientManagerInterface::class),
$this->client->getContainer()->get(AccessTokenManagerInterface::class),
- $this->client->getContainer()->get(RefreshTokenManagerInterface::class)
+ $this->client->getContainer()->get(RefreshTokenManagerInterface::class),
+ $this->client->getContainer()->get(AuthorizationCodeManagerInterface::class)
);
}
diff --git a/Tests/Acceptance/SecurityLayerTest.php b/Tests/Acceptance/SecurityLayerTest.php
index f54b2e23..40f5bd0b 100644
--- a/Tests/Acceptance/SecurityLayerTest.php
+++ b/Tests/Acceptance/SecurityLayerTest.php
@@ -5,6 +5,7 @@
namespace Trikoder\Bundle\OAuth2Bundle\Tests\Acceptance;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
+use Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
@@ -21,7 +22,8 @@ protected function setUp(): void
$this->client->getContainer()->get(ScopeManagerInterface::class),
$this->client->getContainer()->get(ClientManagerInterface::class),
$this->client->getContainer()->get(AccessTokenManagerInterface::class),
- $this->client->getContainer()->get(RefreshTokenManagerInterface::class)
+ $this->client->getContainer()->get(RefreshTokenManagerInterface::class),
+ $this->client->getContainer()->get(AuthorizationCodeManagerInterface::class)
);
}
diff --git a/Tests/Acceptance/TokenEndpointTest.php b/Tests/Acceptance/TokenEndpointTest.php
index 494a2b3d..892bd896 100644
--- a/Tests/Acceptance/TokenEndpointTest.php
+++ b/Tests/Acceptance/TokenEndpointTest.php
@@ -7,6 +7,7 @@
use DateTime;
use Trikoder\Bundle\OAuth2Bundle\Event\UserResolveEvent;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
+use Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
@@ -23,7 +24,8 @@ protected function setUp(): void
$this->client->getContainer()->get(ScopeManagerInterface::class),
$this->client->getContainer()->get(ClientManagerInterface::class),
$this->client->getContainer()->get(AccessTokenManagerInterface::class),
- $this->client->getContainer()->get(RefreshTokenManagerInterface::class)
+ $this->client->getContainer()->get(RefreshTokenManagerInterface::class),
+ $this->client->getContainer()->get(AuthorizationCodeManagerInterface::class)
);
}
@@ -116,6 +118,37 @@ public function testSuccessfulRefreshTokenRequest()
$this->assertNotEmpty($jsonResponse['refresh_token']);
}
+ public function testSuccessfulAuthorizationCodeRequest()
+ {
+ $authCode = $this->client
+ ->getContainer()
+ ->get(AuthorizationCodeManagerInterface::class)
+ ->find(FixtureFactory::FIXTURE_AUTH_CODE);
+
+ timecop_freeze(new DateTime());
+
+ $this->client->request('POST', '/token', [
+ 'client_id' => 'foo',
+ 'client_secret' => 'secret',
+ 'grant_type' => 'authorization_code',
+ 'redirect_uri' => 'https://example.org/oauth2/redirect-uri',
+ 'code' => TestHelper::generateEncryptedAuthCodePayload($authCode),
+ ]);
+
+ timecop_return();
+
+ $response = $this->client->getResponse();
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('application/json; charset=UTF-8', $response->headers->get('Content-Type'));
+
+ $jsonResponse = json_decode($response->getContent(), true);
+
+ $this->assertSame('Bearer', $jsonResponse['token_type']);
+ $this->assertSame(3600, $jsonResponse['expires_in']);
+ $this->assertNotEmpty($jsonResponse['access_token']);
+ }
+
public function testFailedTokenRequest()
{
$this->client->request('GET', '/token');
diff --git a/Tests/Fixtures/FixtureFactory.php b/Tests/Fixtures/FixtureFactory.php
index 06aa66a7..5ba78ecc 100644
--- a/Tests/Fixtures/FixtureFactory.php
+++ b/Tests/Fixtures/FixtureFactory.php
@@ -6,12 +6,15 @@
use DateTime;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
+use Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken;
+use Trikoder\Bundle\OAuth2Bundle\Model\AuthorizationCode;
use Trikoder\Bundle\OAuth2Bundle\Model\Client;
use Trikoder\Bundle\OAuth2Bundle\Model\Grant;
+use Trikoder\Bundle\OAuth2Bundle\Model\RedirectUri;
use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken;
use Trikoder\Bundle\OAuth2Bundle\Model\Scope;
@@ -37,12 +40,18 @@ final class FixtureFactory
public const FIXTURE_REFRESH_TOKEN_REVOKED = '63641841630c2e4d747e0f9ebe12ee04424e322874b8e68ef69fd58f1899ef70beb09733e23928a6';
public const FIXTURE_REFRESH_TOKEN_WITH_SCOPES = 'e47d593ed661840b3633e4577c3261ef57ba225be193b190deb69ee9afefdc19f54f890fbdda59f5';
+ public const FIXTURE_AUTH_CODE = '0aa70e8152259988b3c8e9e8cff604019bb986eb226bd126da189829b95a2be631e2506042064e12';
+ public const FIXTURE_AUTH_CODE_DIFFERENT_CLIENT = 'e8fe264053cb346f4437af05c8cc9036931cfec3a0d5b54bdae349304ca4a83fd2f4590afd51e559';
+ public const FIXTURE_AUTH_CODE_EXPIRED = 'a7bdbeb26c9f095d842f5e5b8e313b24318d6b26728d1c543136727bbe9525f7a7930305a09b7401';
+
public const FIXTURE_CLIENT_FIRST = 'foo';
public const FIXTURE_CLIENT_SECOND = 'bar';
public const FIXTURE_CLIENT_INACTIVE = 'baz_inactive';
public const FIXTURE_CLIENT_RESTRICTED_GRANTS = 'qux_restricted_grants';
public const FIXTURE_CLIENT_RESTRICTED_SCOPES = 'quux_restricted_scopes';
+ public const FIXTURE_CLIENT_FIRST_REDIRECT_URI = 'https://example.org/oauth2/redirect-uri';
+
public const FIXTURE_SCOPE_FIRST = 'fancy';
public const FIXTURE_SCOPE_SECOND = 'rock';
@@ -60,7 +69,8 @@ public static function initializeFixtures(
ScopeManagerInterface $scopeManager,
ClientManagerInterface $clientManager,
AccessTokenManagerInterface $accessTokenManager,
- RefreshTokenManagerInterface $refreshTokenManager
+ RefreshTokenManagerInterface $refreshTokenManager,
+ AuthorizationCodeManagerInterface $authCodeManager
): void {
foreach (self::createScopes() as $scope) {
$scopeManager->save($scope);
@@ -77,6 +87,10 @@ public static function initializeFixtures(
foreach (self::createRefreshTokens($accessTokenManager) as $refreshToken) {
$refreshTokenManager->save($refreshToken);
}
+
+ foreach (self::createAuthorizationCodes($clientManager) as $authorizationCode) {
+ $authCodeManager->save($authorizationCode);
+ }
}
/**
@@ -187,6 +201,40 @@ private static function createRefreshTokens(AccessTokenManagerInterface $accessT
return $refreshTokens;
}
+ /**
+ * @return AuthorizationCode[]
+ */
+ public static function createAuthorizationCodes(ClientManagerInterface $clientManager): array
+ {
+ $authorizationCodes = [];
+
+ $authorizationCodes[] = new AuthorizationCode(
+ self::FIXTURE_AUTH_CODE,
+ new DateTime('+2 minute'),
+ $clientManager->find(self::FIXTURE_CLIENT_FIRST),
+ self::FIXTURE_USER,
+ []
+ );
+
+ $authorizationCodes[] = new AuthorizationCode(
+ self::FIXTURE_AUTH_CODE_DIFFERENT_CLIENT,
+ new DateTime('+2 minute'),
+ $clientManager->find(self::FIXTURE_CLIENT_SECOND),
+ self::FIXTURE_USER,
+ []
+ );
+
+ $authorizationCodes[] = new AuthorizationCode(
+ self::FIXTURE_AUTH_CODE_EXPIRED,
+ new DateTime('-30 minute'),
+ $clientManager->find(self::FIXTURE_CLIENT_FIRST),
+ self::FIXTURE_USER,
+ []
+ );
+
+ return $authorizationCodes;
+ }
+
/**
* @return Client[]
*/
@@ -194,7 +242,8 @@ private static function createClients(): array
{
$clients = [];
- $clients[] = new Client(self::FIXTURE_CLIENT_FIRST, 'secret');
+ $clients[] = (new Client(self::FIXTURE_CLIENT_FIRST, 'secret'))
+ ->setRedirectUris(new RedirectUri(self::FIXTURE_CLIENT_FIRST_REDIRECT_URI));
$clients[] = new Client(self::FIXTURE_CLIENT_SECOND, 'top_secret');
diff --git a/Tests/Fixtures/User.php b/Tests/Fixtures/User.php
index 6242e6c3..806cd63d 100644
--- a/Tests/Fixtures/User.php
+++ b/Tests/Fixtures/User.php
@@ -22,7 +22,7 @@ public function getRoles()
*/
public function getPassword()
{
- return null;
+ return FixtureFactory::FIXTURE_PASSWORD;
}
/**
diff --git a/Tests/Integration/AbstractIntegrationTest.php b/Tests/Integration/AbstractIntegrationTest.php
index ecabddf4..ff6dbd58 100644
--- a/Tests/Integration/AbstractIntegrationTest.php
+++ b/Tests/Integration/AbstractIntegrationTest.php
@@ -4,34 +4,42 @@
namespace Trikoder\Bundle\OAuth2Bundle\Tests\Integration;
+use DateInterval;
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Exception\CryptoException;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Exception\OAuthServerException;
+use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
use League\OAuth2\Server\Grant\PasswordGrant;
use League\OAuth2\Server\Grant\RefreshTokenGrant;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
+use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\ResourceServer;
use Nyholm\Psr7\Factory\Psr17Factory;
+use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Trikoder\Bundle\OAuth2Bundle\Converter\ScopeConverter;
+use Trikoder\Bundle\OAuth2Bundle\League\Entity\User;
use Trikoder\Bundle\OAuth2Bundle\League\Repository\AccessTokenRepository;
+use Trikoder\Bundle\OAuth2Bundle\League\Repository\AuthCodeRepository;
use Trikoder\Bundle\OAuth2Bundle\League\Repository\ClientRepository;
use Trikoder\Bundle\OAuth2Bundle\League\Repository\RefreshTokenRepository;
use Trikoder\Bundle\OAuth2Bundle\League\Repository\ScopeRepository;
use Trikoder\Bundle\OAuth2Bundle\League\Repository\UserRepository;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
+use Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\InMemory\AccessTokenManager;
+use Trikoder\Bundle\OAuth2Bundle\Manager\InMemory\AuthorizationCodeManager;
use Trikoder\Bundle\OAuth2Bundle\Manager\InMemory\ClientManager;
use Trikoder\Bundle\OAuth2Bundle\Manager\InMemory\RefreshTokenManager;
use Trikoder\Bundle\OAuth2Bundle\Manager\InMemory\ScopeManager;
@@ -58,6 +66,11 @@ abstract class AbstractIntegrationTest extends TestCase
*/
protected $accessTokenManager;
+ /**
+ * @var AuthorizationCodeManagerInterface
+ */
+ protected $authCodeManager;
+
/**
* @var RefreshTokenManagerInterface
*/
@@ -92,6 +105,7 @@ protected function setUp(): void
$this->clientManager = new ClientManager();
$this->accessTokenManager = new AccessTokenManager();
$this->refreshTokenManager = new RefreshTokenManager();
+ $this->authCodeManager = new AuthorizationCodeManager();
$this->eventDispatcher = new EventDispatcher();
$scopeConverter = new ScopeConverter();
@@ -100,13 +114,15 @@ protected function setUp(): void
$accessTokenRepository = new AccessTokenRepository($this->accessTokenManager, $this->clientManager, $scopeConverter);
$refreshTokenRepository = new RefreshTokenRepository($this->refreshTokenManager, $this->accessTokenManager);
$userRepository = new UserRepository($this->clientManager, $this->eventDispatcher);
+ $authCodeRepository = new AuthCodeRepository($this->authCodeManager, $this->clientManager, $scopeConverter);
$this->authorizationServer = $this->createAuthorizationServer(
$scopeRepository,
$clientRepository,
$accessTokenRepository,
$refreshTokenRepository,
- $userRepository
+ $userRepository,
+ $authCodeRepository
);
$this->resourceServer = $this->createResourceServer($accessTokenRepository);
@@ -168,7 +184,18 @@ protected function createResourceRequest(string $jwtToken): ServerRequestInterfa
;
}
- protected function handleAuthorizationRequest(ServerRequestInterface $serverRequest): array
+ protected function createAuthorizeRequest(?string $credentials, array $query = []): ServerRequestInterface
+ {
+ $serverRequest = $this
+ ->psrFactory
+ ->createServerRequest('', '')
+ ->withQueryParams($query)
+ ;
+
+ return \is_string($credentials) ? $serverRequest->withHeader('Authorization', sprintf('Basic %s', base64_encode($credentials))) : $serverRequest;
+ }
+
+ protected function handleTokenRequest(ServerRequestInterface $serverRequest): array
{
$response = $this->psrFactory->createResponse();
@@ -192,12 +219,42 @@ protected function handleResourceRequest(ServerRequestInterface $serverRequest):
return $serverRequest;
}
+ protected function handleAuthorizationRequest(ServerRequestInterface $serverRequest, $approved = true): ResponseInterface
+ {
+ $response = $this->psrFactory->createResponse();
+
+ try {
+ $authRequest = $this->authorizationServer->validateAuthorizationRequest($serverRequest);
+ $user = new User();
+ $user->setIdentifier('user');
+ $authRequest->setUser($user);
+ $authRequest->setAuthorizationApproved($approved);
+
+ $response = $this->authorizationServer->completeAuthorizationRequest($authRequest, $response);
+ } catch (OAuthServerException $e) {
+ $response = $e->generateHttpResponse($response);
+ }
+
+ return $response;
+ }
+
+ protected function extractQueryDataFromUri(string $uri): array
+ {
+ $uriObject = $this->psrFactory->createUri($uri);
+
+ $data = [];
+ parse_str($uriObject->getQuery(), $data);
+
+ return $data;
+ }
+
private function createAuthorizationServer(
ScopeRepositoryInterface $scopeRepository,
ClientRepositoryInterface $clientRepository,
AccessTokenRepositoryInterface $accessTokenRepository,
RefreshTokenRepositoryInterface $refreshTokenRepository,
- UserRepositoryInterface $userRepository
+ UserRepositoryInterface $userRepository,
+ AuthCodeRepositoryInterface $authCodeRepository
): AuthorizationServer {
$authorizationServer = new AuthorizationServer(
$clientRepository,
@@ -210,6 +267,7 @@ private function createAuthorizationServer(
$authorizationServer->enableGrantType(new ClientCredentialsGrant());
$authorizationServer->enableGrantType(new RefreshTokenGrant($refreshTokenRepository));
$authorizationServer->enableGrantType(new PasswordGrant($userRepository, $refreshTokenRepository));
+ $authorizationServer->enableGrantType(new AuthCodeGrant($authCodeRepository, $refreshTokenRepository, new DateInterval('PT10M')));
return $authorizationServer;
}
diff --git a/Tests/Integration/AuthorizationServerTest.php b/Tests/Integration/AuthorizationServerTest.php
index 224e350b..eb68b3ed 100644
--- a/Tests/Integration/AuthorizationServerTest.php
+++ b/Tests/Integration/AuthorizationServerTest.php
@@ -21,7 +21,8 @@ protected function setUp(): void
$this->scopeManager,
$this->clientManager,
$this->accessTokenManager,
- $this->refreshTokenManager
+ $this->refreshTokenManager,
+ $this->authCodeManager
);
}
@@ -31,7 +32,7 @@ public function testSuccessfulAuthorizationThroughHeaders(): void
'grant_type' => 'client_credentials',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Assert that we got something that looks like a normal response.
$this->assertArrayHasKey('token_type', $response);
@@ -45,7 +46,7 @@ public function testSuccessfulAuthorizationThroughBody(): void
'grant_type' => 'client_credentials',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Assert that we got something that looks like a normal response.
$this->assertArrayHasKey('token_type', $response);
@@ -57,7 +58,7 @@ public function testMissingAuthorizationCredentials(): void
'grant_type' => 'client_credentials',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
@@ -71,7 +72,7 @@ public function testInvalidAuthorizationCredentials(): void
'grant_type' => 'client_credentials',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_client', $response['error']);
@@ -84,7 +85,7 @@ public function testMissingClient(): void
'grant_type' => 'client_credentials',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_client', $response['error']);
@@ -97,7 +98,7 @@ public function testInactiveClient(): void
'grant_type' => 'client_credentials',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_client', $response['error']);
@@ -110,7 +111,7 @@ public function testRestrictedGrantClient(): void
'grant_type' => 'client_credentials',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_client', $response['error']);
@@ -124,7 +125,7 @@ public function testRestrictedScopeClient(): void
'scope' => 'fancy rock',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_scope', $response['error']);
@@ -138,7 +139,7 @@ public function testInvalidGrantType(): void
'grant_type' => 'non_existing',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('unsupported_grant_type', $response['error']);
@@ -153,7 +154,7 @@ public function testInvalidScope(): void
'scope' => 'non_existing',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_scope', $response['error']);
@@ -169,7 +170,7 @@ public function testValidClientCredentialsGrant(): void
timecop_freeze(new DateTime());
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
timecop_return();
@@ -193,7 +194,7 @@ public function testValidClientCredentialsGrantWithScope(): void
timecop_freeze(new DateTime());
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
timecop_return();
@@ -224,7 +225,7 @@ public function testValidClientCredentialsGrantWithInheritedScope(): void
timecop_freeze(new DateTime());
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
timecop_return();
@@ -256,7 +257,7 @@ public function testValidClientCredentialsGrantWithRequestedScope(): void
timecop_freeze(new DateTime());
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
timecop_return();
@@ -293,7 +294,7 @@ public function testValidPasswordGrant(): void
timecop_freeze(new DateTime());
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
timecop_return();
@@ -325,7 +326,7 @@ public function testInvalidCredentialsPasswordGrant(): void
'password' => 'pass',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_credentials', $response['error']);
@@ -339,7 +340,7 @@ public function testMissingUsernameFieldPasswordGrant(): void
'password' => 'pass',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
@@ -354,7 +355,7 @@ public function testMissingPasswordFieldPasswordGrant(): void
'username' => 'user',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
@@ -374,7 +375,7 @@ public function testValidRefreshGrant(): void
timecop_freeze(new DateTime());
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
timecop_return();
@@ -405,7 +406,7 @@ public function testDifferentClientRefreshGrant(): void
'refresh_token' => TestHelper::generateEncryptedPayload($existingRefreshToken),
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
@@ -423,7 +424,7 @@ public function testDifferentScopeRefreshGrant(): void
'refresh_token' => TestHelper::generateEncryptedPayload($existingRefreshToken),
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_scope', $response['error']);
@@ -440,7 +441,7 @@ public function testExpiredRefreshGrant(): void
'refresh_token' => TestHelper::generateEncryptedPayload($existingRefreshToken),
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
@@ -457,7 +458,7 @@ public function testRevokedRefreshGrant(): void
'refresh_token' => TestHelper::generateEncryptedPayload($existingRefreshToken),
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
@@ -471,7 +472,7 @@ public function testMissingPayloadRefreshGrant(): void
'grant_type' => 'refresh_token',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
@@ -486,11 +487,241 @@ public function testInvalidPayloadRefreshGrant(): void
'refresh_token' => 'invalid',
]);
- $response = $this->handleAuthorizationRequest($request);
+ $response = $this->handleTokenRequest($request);
// Response assertions.
$this->assertSame('invalid_request', $response['error']);
$this->assertSame('The refresh token is invalid.', $response['message']);
$this->assertSame('Cannot decrypt the refresh token', $response['hint']);
}
+
+ public function testSuccessfulCodeRequest(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'foo',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request);
+
+ // Response assertions.
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertTrue($response->hasHeader('Location'));
+ $this->assertStringStartsWith(FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI, $response->getHeaderLine('Location'));
+ $queryData = $this->extractQueryDataFromUri($response->getHeaderLine('Location'));
+ $this->assertArrayHasKey('code', $queryData);
+ }
+
+ public function testSuccessfulCodeRequestWithState(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'foo',
+ 'state' => 'quzbaz',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request);
+
+ // Response assertions.
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertTrue($response->hasHeader('Location'));
+ $this->assertStringStartsWith(FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI, $response->getHeaderLine('Location'));
+ $queryData = $this->extractQueryDataFromUri($response->getHeaderLine('Location'));
+ $this->assertArrayHasKey('code', $queryData);
+ $this->assertSame('quzbaz', $queryData['state']);
+ }
+
+ public function testSuccessfulCodeRequestWithRedirectUri(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'foo',
+ 'redirect_uri' => 'https://example.org/oauth2/redirect-uri',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request);
+
+ // Response assertions.
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertTrue($response->hasHeader('Location'));
+ $this->assertStringStartsWith(FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI, $response->getHeaderLine('Location'));
+ $queryData = $this->extractQueryDataFromUri($response->getHeaderLine('Location'));
+ $this->assertArrayHasKey('code', $queryData);
+ }
+
+ public function testCodeRequestWithInvalidScope(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'foo',
+ 'scope' => 'non_existing',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request);
+
+ // Response assertions.
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertTrue($response->hasHeader('Location'));
+ $this->assertStringStartsWith(FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI, $response->getHeaderLine('Location'));
+ $queryData = $this->extractQueryDataFromUri($response->getHeaderLine('Location'));
+ $this->assertSame('invalid_scope', $queryData['error']);
+ $this->assertSame('The requested scope is invalid, unknown, or malformed', $queryData['message']);
+ $this->assertSame('Check the `non_existing` scope', $queryData['hint']);
+ }
+
+ public function testCodeRequestWithInvalidRedirectUri(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'foo',
+ 'redirect_uri' => 'https://example.org/oauth2/other-uri',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request);
+
+ // Response assertions.
+ $this->assertSame(401, $response->getStatusCode());
+ $responseData = json_decode((string) $response->getBody(), true);
+ $this->assertSame('invalid_client', $responseData['error']);
+ $this->assertSame('Client authentication failed', $responseData['message']);
+ }
+
+ public function testDeniedCodeRequest(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'foo',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request, false);
+
+ // Response assertions.
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertTrue($response->hasHeader('Location'));
+ $this->assertStringStartsWith(FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI, $response->getHeaderLine('Location'));
+ $queryData = $this->extractQueryDataFromUri($response->getHeaderLine('Location'));
+ $this->assertSame('access_denied', $queryData['error']);
+ $this->assertSame('The resource owner or authorization server denied the request.', $queryData['message']);
+ $this->assertSame('The user denied the request', $queryData['hint']);
+ }
+
+ public function testCodeRequestWithMissingClient(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'yolo',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request, false);
+
+ // Response assertions.
+ $this->assertSame(401, $response->getStatusCode());
+ $responseData = json_decode((string) $response->getBody(), true);
+ $this->assertSame('invalid_client', $responseData['error']);
+ $this->assertSame('Client authentication failed', $responseData['message']);
+ }
+
+ public function testCodeRequestWithInactiveClient(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'baz_inactive',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request, false);
+
+ // Response assertions.
+ $this->assertSame(401, $response->getStatusCode());
+ $responseData = json_decode((string) $response->getBody(), true);
+ $this->assertSame('invalid_client', $responseData['error']);
+ $this->assertSame('Client authentication failed', $responseData['message']);
+ }
+
+ public function testCodeRequestWithRestrictedGrantClient(): void
+ {
+ $request = $this->createAuthorizeRequest(null, [
+ 'response_type' => 'code',
+ 'client_id' => 'qux_restricted',
+ ]);
+
+ $response = $this->handleAuthorizationRequest($request, false);
+
+ // Response assertions.
+ $this->assertSame(401, $response->getStatusCode());
+ $responseData = json_decode((string) $response->getBody(), true);
+ $this->assertSame('invalid_client', $responseData['error']);
+ $this->assertSame('Client authentication failed', $responseData['message']);
+ }
+
+ public function testSuccessfulAuthorizationWithCode(): void
+ {
+ $existingAuthCode = $this->authCodeManager->find(FixtureFactory::FIXTURE_AUTH_CODE);
+
+ $request = $this->createAuthorizationRequest('foo:secret', [
+ 'grant_type' => 'authorization_code',
+ 'code' => TestHelper::generateEncryptedAuthCodePayload($existingAuthCode),
+ 'redirect_uri' => 'https://example.org/oauth2/redirect-uri',
+ ]);
+
+ $response = $this->handleTokenRequest($request);
+ $accessToken = $this->getAccessToken($response['access_token']);
+
+ $this->assertSame('Bearer', $response['token_type']);
+ $this->assertSame(3600, $response['expires_in']);
+ $this->assertInstanceOf(AccessToken::class, $accessToken);
+ $this->assertSame('foo', $accessToken->getClient()->getIdentifier());
+ }
+
+ public function testFailedAuthorizationWithCodeForOtherClient(): void
+ {
+ $existingAuthCode = $this->authCodeManager->find(FixtureFactory::FIXTURE_AUTH_CODE_DIFFERENT_CLIENT);
+
+ $request = $this->createAuthorizationRequest('foo:secret', [
+ 'grant_type' => 'authorization_code',
+ 'code' => TestHelper::generateEncryptedAuthCodePayload($existingAuthCode),
+ 'redirect_uri' => 'https://example.org/oauth2/redirect-uri',
+ ]);
+
+ $response = $this->handleTokenRequest($request);
+
+ // Response assertions.
+ $this->assertSame('invalid_request', $response['error']);
+ $this->assertSame('The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.', $response['message']);
+ $this->assertSame('Authorization code was not issued to this client', $response['hint']);
+ }
+
+ public function testFailedAuthorizationWithExpiredCode(): void
+ {
+ $existingAuthCode = $this->authCodeManager->find(FixtureFactory::FIXTURE_AUTH_CODE_EXPIRED);
+
+ $request = $this->createAuthorizationRequest('foo:secret', [
+ 'grant_type' => 'authorization_code',
+ 'code' => TestHelper::generateEncryptedAuthCodePayload($existingAuthCode),
+ 'redirect_uri' => 'https://example.org/oauth2/redirect-uri',
+ ]);
+
+ $response = $this->handleTokenRequest($request);
+
+ // Response assertions.
+ $this->assertSame('invalid_request', $response['error']);
+ $this->assertSame('The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.', $response['message']);
+ $this->assertSame('Authorization code has expired', $response['hint']);
+ }
+
+ public function testFailedAuthorizationWithInvalidRedirectUri(): void
+ {
+ $existingAuthCode = $this->authCodeManager->find(FixtureFactory::FIXTURE_AUTH_CODE);
+
+ $request = $this->createAuthorizationRequest('foo:secret', [
+ 'grant_type' => 'authorization_code',
+ 'code' => TestHelper::generateEncryptedAuthCodePayload($existingAuthCode),
+ 'redirect_uri' => 'https://example.org/oauth2/other-uri',
+ ]);
+
+ $response = $this->handleTokenRequest($request);
+
+ // Response assertions.
+ $this->assertSame('invalid_client', $response['error']);
+ $this->assertSame('Client authentication failed', $response['message']);
+ }
}
diff --git a/Tests/Integration/ResourceServerTest.php b/Tests/Integration/ResourceServerTest.php
index ec866617..2ad1977f 100644
--- a/Tests/Integration/ResourceServerTest.php
+++ b/Tests/Integration/ResourceServerTest.php
@@ -17,7 +17,8 @@ protected function setUp(): void
$this->scopeManager,
$this->clientManager,
$this->accessTokenManager,
- $this->refreshTokenManager
+ $this->refreshTokenManager,
+ $this->authCodeManager
);
}
diff --git a/Tests/TestHelper.php b/Tests/TestHelper.php
index 3a55ff7f..a69fc0fa 100644
--- a/Tests/TestHelper.php
+++ b/Tests/TestHelper.php
@@ -10,10 +10,12 @@
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
+use Trikoder\Bundle\OAuth2Bundle\Converter\ScopeConverter;
use Trikoder\Bundle\OAuth2Bundle\League\Entity\AccessToken as AccessTokenEntity;
use Trikoder\Bundle\OAuth2Bundle\League\Entity\Client as ClientEntity;
use Trikoder\Bundle\OAuth2Bundle\League\Entity\Scope as ScopeEntity;
use Trikoder\Bundle\OAuth2Bundle\Model\AccessToken as AccessTokenModel;
+use Trikoder\Bundle\OAuth2Bundle\Model\AuthorizationCode as AuthorizationCodeModel;
use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken as RefreshTokenModel;
final class TestHelper
@@ -40,6 +42,26 @@ public static function generateEncryptedPayload(RefreshTokenModel $refreshToken)
}
}
+ public static function generateEncryptedAuthCodePayload(AuthorizationCodeModel $authCode): ?string
+ {
+ $payload = json_encode([
+ 'client_id' => $authCode->getClient()->getIdentifier(),
+ 'redirect_uri' => (string) $authCode->getClient()->getRedirectUris()[0],
+ 'auth_code_id' => $authCode->getIdentifier(),
+ 'scopes' => (new ScopeConverter())->toDomainArray($authCode->getScopes()),
+ 'user_id' => $authCode->getUserIdentifier(),
+ 'expire_time' => $authCode->getExpiryDateTime()->getTimestamp(),
+ 'code_challenge' => null,
+ 'code_challenge_method' => null,
+ ]);
+
+ try {
+ return Crypto::encryptWithPassword($payload, self::ENCRYPTION_KEY);
+ } catch (CryptoException $e) {
+ return null;
+ }
+ }
+
public static function generateJwtToken(AccessTokenModel $accessToken): string
{
$clientEntity = new ClientEntity();
diff --git a/Tests/TestKernel.php b/Tests/TestKernel.php
index a7ca02cc..3c1eb473 100644
--- a/Tests/TestKernel.php
+++ b/Tests/TestKernel.php
@@ -18,6 +18,7 @@
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface;
+use Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface;
use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface;
@@ -225,6 +226,15 @@ private function exposeManagerServices(ContainerBuilder $container): void
)
->setPublic(true)
;
+
+ $container
+ ->getDefinition(
+ $container
+ ->getAlias(AuthorizationCodeManagerInterface::class)
+ ->setPublic(true)
+ )
+ ->setPublic(true)
+ ;
}
private function configurePsrHttpFactory(ContainerBuilder $container): void