├── .gitignore
├── .github
└── workflows
│ └── continuous-integration.yml
├── phpcs.xml.dist
├── phpunit.xml.dist
├── src
├── Exception
│ ├── ExceptionInterface.php
│ ├── RuntimeException.php
│ ├── InvalidAccessTokenException.php
│ └── OAuth2Exception.php
├── Repository
│ ├── AccessTokenRepositoryInterface.php
│ ├── RefreshTokenRepositoryInterface.php
│ ├── AuthorizationCodeRepositoryInterface.php
│ ├── ClientRepositoryInterface.php
│ ├── TokenRepositoryInterface.php
│ └── ScopeRepositoryInterface.php
├── Grant
│ ├── AuthorizationServerAwareInterface.php
│ ├── AbstractGrant.php
│ ├── GrantInterface.php
│ ├── ClientCredentialsGrant.php
│ └── RefreshTokenGrant.php
├── ModuleConfig.php
├── Model
│ ├── TokenOwnerInterface.php
│ ├── AccessToken.php
│ ├── RefreshToken.php
│ ├── AuthorizationCode.php
│ └── Scope.php
├── Container
│ ├── ServerOptionsFactory.php
│ ├── ResourceServerFactory.php
│ ├── ScopeServiceFactory.php
│ ├── ClientServiceFactory.php
│ ├── ClientCredentialsGrantFactory.php
│ ├── TokenRequestMiddlewareFactory.php
│ ├── RevocationRequestMiddlewareFactory.php
│ ├── ResourceServerMiddlewareFactory.php
│ ├── AuthorizationRequestMiddlewareFactory.php
│ ├── AccessTokenServiceFactory.php
│ ├── RefreshTokenGrantFactory.php
│ ├── AuthorizationCodeServiceFactory.php
│ ├── AuthorizationGrantFactory.php
│ ├── RefreshTokenServiceFactory.php
│ ├── PasswordGrantFactory.php
│ └── AuthorizationServerFactory.php
├── ResourceServerInterface.php
├── Middleware
│ ├── TokenRequestMiddleware.php
│ ├── RevocationRequestMiddleware.php
│ ├── AuthorizationRequestMiddleware.php
│ └── ResourceServerMiddleware.php
├── Service
│ ├── ScopeService.php
│ ├── AccessTokenService.php
│ ├── RefreshTokenService.php
│ ├── ClientService.php
│ ├── AuthorizationCodeService.php
│ └── AbstractTokenService.php
├── AuthorizationServerInterface.php
└── ResourceServer.php
├── test
├── src
│ ├── Asset
│ │ └── SomeToken.php
│ ├── ModuleConfigTest.php
│ ├── Container
│ │ ├── ServerOptionsFactoryTest.php
│ │ ├── ScopeServiceFactoryTest.php
│ │ ├── ClientServiceFactoryTest.php
│ │ ├── ClientCredentialsGrantFactoryTest.php
│ │ ├── ResourceServerFactoryTest.php
│ │ ├── TokenRequestMiddlewareFactoryTest.php
│ │ ├── RevocationRequestMiddlewareFactoryTest.php
│ │ ├── ResourceServerMiddlewareFactoryTest.php
│ │ ├── AuthorizationRequestMiddlewareFactoryTest.php
│ │ ├── RefreshTokenGrantFactoryTest.php
│ │ ├── AccessTokenServiceFactoryTest.php
│ │ ├── RefreshTokenServiceFactoryTest.php
│ │ ├── AuthorizationGrantFactoryTest.php
│ │ ├── AuthorizationCodeServiceFactoryTest.php
│ │ ├── AuthorizationServerFactoryTest.php
│ │ └── PasswordGrantFactoryTest.php
│ ├── Exception
│ │ ├── InvalidAccessTokenExceptionTest.php
│ │ └── OAuth2ExceptionTest.php
│ ├── Middleware
│ │ ├── AuthorizationRequestMiddlewareTest.php
│ │ ├── TokenRequestMiddlewareTest.php
│ │ ├── RevocationRequestMiddlewareTest.php
│ │ └── ResourceServerMiddlewareTest.php
│ ├── Service
│ │ ├── ScopeServiceTest.php
│ │ └── ClientServiceTest.php
│ ├── Model
│ │ ├── ScopeTest.php
│ │ ├── AbstractTokenTest.php
│ │ └── AuthorizationCodeTest.php
│ ├── Options
│ │ └── ServerOptionsTest.php
│ └── Grant
│ │ ├── ClientCredentialsGrantTest.php
│ │ └── AbstractGrantTest.php
└── Bootstrap.php
├── LICENSE
├── composer.json
├── config
├── config.global.php
└── dependencies.global.php
└── CHANGELOG.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /.phpcs-cache
2 | /.phpunit.result.cache
3 | /clover.xml
4 | /composer.lock
5 | /coveralls-upload.json
6 | /docs/html/
7 | /laminas-mkdoc-theme.tgz
8 | /laminas-mkdoc-theme/
9 | /phpunit.xml
10 | /vendor/
11 |
--------------------------------------------------------------------------------
/.github/workflows/continuous-integration.yml:
--------------------------------------------------------------------------------
1 | name: "Continuous Integration"
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | tags:
8 |
9 | jobs:
10 | ci:
11 | uses: laminas/workflow-continuous-integration/.github/workflows/continuous-integration.yml@1.x
12 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | src
17 | test
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./test/
6 |
7 |
8 |
9 |
10 |
11 | disable
12 |
13 |
14 |
15 |
16 |
17 | ./src
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Exception/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 | get('config');
35 | $options = $config['zfr_oauth2_server'] ?? [];
36 |
37 | return ServerOptions::fromArray($options);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, ZF-Commons Contributors
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | Neither the name of the ZF-Commons nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/test/Bootstrap.php:
--------------------------------------------------------------------------------
1 | add('ZfrOAuth2Test\\', __DIR__);
39 |
40 | unset($files, $file, $loader);
41 |
--------------------------------------------------------------------------------
/src/Container/ResourceServerFactory.php:
--------------------------------------------------------------------------------
1 | get(AccessTokenService::class);
37 |
38 | return new ResourceServer($tokenService);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Container/ScopeServiceFactory.php:
--------------------------------------------------------------------------------
1 | get(ScopeRepositoryInterface::class);
37 |
38 | return new ScopeService($scopeRepository);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Container/ClientServiceFactory.php:
--------------------------------------------------------------------------------
1 | get(ClientRepositoryInterface::class);
37 |
38 | return new ClientService($clientRepository);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Container/ClientCredentialsGrantFactory.php:
--------------------------------------------------------------------------------
1 | get(AccessTokenService::class);
37 |
38 | return new ClientCredentialsGrant($accessTokenService);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Container/TokenRequestMiddlewareFactory.php:
--------------------------------------------------------------------------------
1 | get(AuthorizationServerInterface::class);
37 |
38 | return new TokenRequestMiddleware($authorizationServer);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Container/RevocationRequestMiddlewareFactory.php:
--------------------------------------------------------------------------------
1 | get(AuthorizationServerInterface::class);
37 |
38 | return new RevocationRequestMiddleware($authorizationServer);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Exception/InvalidAccessTokenException.php:
--------------------------------------------------------------------------------
1 | code = $code;
38 | }
39 |
40 | public static function invalidToken(string $description): InvalidAccessTokenException
41 | {
42 | return new self($description, 'invalid_token');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Model/AccessToken.php:
--------------------------------------------------------------------------------
1 | expiresAt && parent::isExpired();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Model/RefreshToken.php:
--------------------------------------------------------------------------------
1 | expiresAt && parent::isExpired();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/test/src/ModuleConfigTest.php:
--------------------------------------------------------------------------------
1 | __invoke();
46 |
47 | $this->assertIsArray($config);
48 | $this->assertArrayHasKey('zfr_oauth2_server', $config);
49 | $this->assertArrayHasKey('dependencies', $config);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Container/ResourceServerMiddlewareFactory.php:
--------------------------------------------------------------------------------
1 | get(ResourceServerInterface::class);
38 | /** @var ServerOptions $serverOptions */
39 | $serverOptions = $container->get(ServerOptions::class);
40 |
41 | return new ResourceServerMiddleware($resourceServer, $serverOptions->getTokenRequestAttribute());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zfr/zfr-oauth2-server",
3 | "description": "PHP library to create an OAuth 2 server",
4 | "type": "library",
5 | "license": "MIT",
6 | "keywords": [
7 | "oauth",
8 | "oauth 2",
9 | "server"
10 | ],
11 | "homepage": "http://www.github.com/zf-fr/zfr-oauth2-server",
12 | "authors": [
13 | {
14 | "name": "Michaël Gallego",
15 | "email": "mic.gallego@gmail.com",
16 | "homepage": "http://www.michaelgallego.fr"
17 | },
18 | {
19 | "name": "Bas Kamer",
20 | "email": "baskamer@gmail.com",
21 | "homepage": "https://baskamer.nl"
22 | }
23 | ],
24 | "config": {
25 | "sort-packages": true,
26 | "allow-plugins": {
27 | "dealerdirect/phpcodesniffer-composer-installer": true
28 | }
29 | },
30 | "require": {
31 | "php": "^7.4 || ^8.0",
32 | "laminas/laminas-diactoros": "^2.6",
33 | "nesbot/carbon": "^2.62",
34 | "psr/container": "^1.0 || ^2.0",
35 | "psr/http-server-middleware": "^1.0",
36 | "ramsey/uuid": "^3.1 || ^4.0",
37 | "roave/security-advisories": "dev-master"
38 | },
39 | "require-dev": {
40 | "friendsofphp/php-cs-fixer": "^2.1",
41 | "laminas/laminas-coding-standard": "^2.4",
42 | "php-mock/php-mock-phpunit": "^2.6",
43 | "phpunit/phpunit": "^9.5.5"
44 | },
45 | "autoload": {
46 | "psr-4": {
47 | "ZfrOAuth2\\Server\\": "src/"
48 | }
49 | },
50 | "autoload-dev": {
51 | "psr-4": {
52 | "ZfrOAuth2Test\\Server\\": "test/src/"
53 | }
54 | },
55 | "extra": {
56 | "branch-alias": {
57 | "dev-master": "0.10.x-dev"
58 | }
59 | },
60 | "scripts": {
61 | "check": [
62 | "@cs-check",
63 | "@test"
64 | ],
65 | "cs-check": "phpcs",
66 | "cs-fix": "phpcbf",
67 | "test": "phpunit --colors=always",
68 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/ResourceServerInterface.php:
--------------------------------------------------------------------------------
1 | get(AuthorizationServerInterface::class);
38 | /** @var ServerOptions $serverOptions */
39 | $serverOptions = $container->get(ServerOptions::class);
40 |
41 | return new AuthorizationRequestMiddleware($authorizationServer, $serverOptions->getOwnerRequestAttribute());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/test/src/Container/ServerOptionsFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
38 |
39 | $container->expects($this->once())
40 | ->method('get')
41 | ->with('config')
42 | ->willReturn(['zfr_oauth2_server' => []]);
43 |
44 | $factory = new ServerOptionsFactory();
45 | $service = $factory($container);
46 |
47 | $this->assertInstanceOf(ServerOptions::class, $service);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Middleware/TokenRequestMiddleware.php:
--------------------------------------------------------------------------------
1 | authorizationServer = $authorizationServer;
40 | }
41 |
42 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
43 | {
44 | return $this->authorizationServer->handleTokenRequest($request);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Middleware/RevocationRequestMiddleware.php:
--------------------------------------------------------------------------------
1 | authorizationServer = $authorizationServer;
40 | }
41 |
42 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
43 | {
44 | return $this->authorizationServer->handleRevocationRequest($request);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Container/AccessTokenServiceFactory.php:
--------------------------------------------------------------------------------
1 | get(ServerOptions::class);
39 |
40 | /** @var AccessTokenRepositoryInterface $tokenRepository */
41 | $tokenRepository = $container->get(AccessTokenRepositoryInterface::class);
42 |
43 | /** @var ScopeService $scopeService */
44 | $scopeService = $container->get(ScopeService::class);
45 |
46 | return new AccessTokenService($tokenRepository, $scopeService, $serverOptions);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Container/RefreshTokenGrantFactory.php:
--------------------------------------------------------------------------------
1 | get(ServerOptions::class);
39 |
40 | /** @var AccessTokenService $accessTokenService */
41 | $accessTokenService = $container->get(AccessTokenService::class);
42 |
43 | /** @var RefreshTokenService $refreshTokenService */
44 | $refreshTokenService = $container->get(RefreshTokenService::class);
45 |
46 | return new RefreshTokenGrant($accessTokenService, $refreshTokenService, $serverOptions);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/src/Container/ScopeServiceFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
39 |
40 | $container->expects($this->once())
41 | ->method('get')
42 | ->with(ScopeRepositoryInterface::class)
43 | ->willReturn($this->createMock(ScopeRepositoryInterface::class));
44 |
45 | $factory = new ScopeServiceFactory();
46 | $service = $factory($container);
47 |
48 | $this->assertInstanceOf(ScopeService::class, $service);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/src/Container/ClientServiceFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
39 |
40 | $container->expects($this->once())
41 | ->method('get')
42 | ->with(ClientRepositoryInterface::class)
43 | ->willReturn($this->createMock(ClientRepositoryInterface::class));
44 |
45 | $factory = new ClientServiceFactory();
46 | $service = $factory($container);
47 |
48 | $this->assertInstanceOf(ClientService::class, $service);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/src/Exception/InvalidAccessTokenExceptionTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(InvalidAccessTokenException::class, $exception);
41 | $this->assertSame('description', $exception->getMessage());
42 | $this->assertSame($expectedErrorCode, $exception->getCode());
43 | }
44 |
45 | public function dataproviderErrorsCode(): array
46 | {
47 | return [
48 | ['invalidToken', 'invalid_token'],
49 | ];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Container/AuthorizationCodeServiceFactory.php:
--------------------------------------------------------------------------------
1 | get(ServerOptions::class);
39 |
40 | /** @var AuthorizationCodeRepositoryInterface $tokenRepository */
41 | $tokenRepository = $container->get(AuthorizationCodeRepositoryInterface::class);
42 |
43 | /** @var ScopeService $scopeService */
44 | $scopeService = $container->get(ScopeService::class);
45 |
46 | return new AuthorizationCodeService($tokenRepository, $scopeService, $serverOptions);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/src/Container/ClientCredentialsGrantFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
39 |
40 | $container->expects($this->once())
41 | ->method('get')
42 | ->with(AccessTokenService::class)
43 | ->willReturn($this->createMock(AccessTokenService::class));
44 |
45 | $factory = new ClientCredentialsGrantFactory();
46 | $service = $factory($container);
47 |
48 | $this->assertInstanceOf(ClientCredentialsGrant::class, $service);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/src/Container/ResourceServerFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
39 | $tokenService = $this->createMock(AccessTokenService::class);
40 |
41 | $container->expects($this->once())
42 | ->method('get')
43 | ->with(AccessTokenService::class)
44 | ->willReturn($tokenService);
45 |
46 | $factory = new ResourceServerFactory();
47 | $service = $factory($container);
48 |
49 | $this->assertInstanceOf(ResourceServerInterface::class, $service);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Container/AuthorizationGrantFactory.php:
--------------------------------------------------------------------------------
1 | get(AuthorizationCodeService::class);
39 |
40 | /** @var AccessTokenService $accessTokenService */
41 | $accessTokenService = $container->get(AccessTokenService::class);
42 |
43 | /** @var RefreshTokenService $refreshTokenService */
44 | $refreshTokenService = $container->get(RefreshTokenService::class);
45 |
46 | return new AuthorizationGrant($authorizationCodeService, $accessTokenService, $refreshTokenService);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/src/Container/TokenRequestMiddlewareFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
39 |
40 | $container->expects($this->once())
41 | ->method('get')
42 | ->with(AuthorizationServerInterface::class)
43 | ->willReturn($this->createMock(AuthorizationServerInterface::class));
44 |
45 | $factory = new TokenRequestMiddlewareFactory();
46 | $service = $factory($container);
47 |
48 | $this->assertInstanceOf(TokenRequestMiddleware::class, $service);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Container/RefreshTokenServiceFactory.php:
--------------------------------------------------------------------------------
1 | get(ServerOptions::class);
40 |
41 | /** @var AuthorizationCodeRepositoryInterface $tokenRepository */
42 | $tokenRepository = $container->get(RefreshTokenRepositoryInterface::class);
43 |
44 | /** @var ScopeService $scopeService */
45 | $scopeService = $container->get(ScopeService::class);
46 |
47 | return new RefreshTokenService($tokenRepository, $scopeService, $serverOptions);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config/config.global.php:
--------------------------------------------------------------------------------
1 | [
23 | /**
24 | * Various tokens TTL
25 | */
26 | 'authorization_code_ttl' => 120,
27 | 'access_token_ttl' => 3600,
28 | 'refresh_token_ttl' => 86400,
29 |
30 | /**
31 | * Rotate the refresh token option while refreshing an access token
32 | */
33 | 'rotate_refresh_tokens' => false,
34 |
35 | /**
36 | * Revoke the rotated refresh token while refreshing an access token
37 | */
38 | 'revoke_rotated_refresh_tokens' => true,
39 |
40 | /**
41 | * Registered grants for this server
42 | */
43 | 'grants' => [],
44 |
45 | /**
46 | * A callable used to validate the username and password when using the
47 | * password grant
48 | */
49 | 'owner_callable' => null,
50 |
51 | /**
52 | * Attribute that the AuthorizationRequestMiddleware expects the ZfrOAuth2\Server\Model\TokenOwnerInterface
53 | * to be present on
54 | */
55 | 'owner_request_attribute' => 'owner',
56 | ],
57 | ];
58 |
--------------------------------------------------------------------------------
/test/src/Container/RevocationRequestMiddlewareFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
39 |
40 | $container->expects($this->once())
41 | ->method('get')
42 | ->with(AuthorizationServerInterface::class)
43 | ->willReturn($this->createMock(AuthorizationServerInterface::class));
44 |
45 | $factory = new RevocationRequestMiddlewareFactory();
46 | $service = $factory($container);
47 |
48 | $this->assertInstanceOf(RevocationRequestMiddleware::class, $service);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Service/ScopeService.php:
--------------------------------------------------------------------------------
1 | scopeRepository = $scopeRepository;
39 | }
40 |
41 | /**
42 | * Create a new scope
43 | */
44 | public function createScope(Scope $scope): Scope
45 | {
46 | return $this->scopeRepository->save($scope);
47 | }
48 |
49 | /**
50 | * Get all the scopes
51 | *
52 | * @return Scope[]
53 | */
54 | public function getAll(): array
55 | {
56 | return $this->scopeRepository->findAllScopes();
57 | }
58 |
59 | /**
60 | * Get all the default scopes
61 | *
62 | * @return Scope[]
63 | */
64 | public function getDefaultScopes(): array
65 | {
66 | return $this->scopeRepository->findDefaultScopes();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Container/PasswordGrantFactory.php:
--------------------------------------------------------------------------------
1 | get(ServerOptions::class);
41 |
42 | $ownerCallable = $options->getOwnerCallable();
43 | $ownerCallable = is_string($ownerCallable) ? $container->get($ownerCallable) : $ownerCallable;
44 |
45 | /** @var AccessTokenService $accessTokenService */
46 | $accessTokenService = $container->get(AccessTokenService::class);
47 |
48 | /** @var RefreshTokenService $refreshTokenService */
49 | $refreshTokenService = $container->get(RefreshTokenService::class);
50 |
51 | return new PasswordGrant($accessTokenService, $refreshTokenService, $ownerCallable);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Middleware/AuthorizationRequestMiddleware.php:
--------------------------------------------------------------------------------
1 | authorizationServer = $authorizationServer;
44 | $this->ownerRequestAttribute = $ownerRequestAttribute;
45 | }
46 |
47 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
48 | {
49 | $owner = $request->getAttribute($this->ownerRequestAttribute);
50 |
51 | return $this->authorizationServer->handleAuthorizationRequest($request, $owner);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/test/src/Middleware/AuthorizationRequestMiddlewareTest.php:
--------------------------------------------------------------------------------
1 | createMock(AuthorizationServerInterface::class);
40 | $middleware = new AuthorizationRequestMiddleware($authorizationServer, 'owner');
41 |
42 | $request = $this->createMock(RequestInterface::class);
43 | $handler = $this->createMock(RequestHandlerInterface::class);
44 |
45 | $authorizationServer->expects($this->once())
46 | ->method('handleAuthorizationRequest')
47 | ->with($request)
48 | ->willReturn($this->createMock(ResponseInterface::class));
49 |
50 | $middleware->process($request, $handler);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/test/src/Container/ResourceServerMiddlewareFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
40 |
41 | $container
42 | ->expects($this->exactly(2))
43 | ->method('get')
44 | ->withConsecutive([ResourceServerInterface::class], [ServerOptions::class])
45 | ->will(
46 | $this->onConsecutiveCalls(
47 | $this->createMock(ResourceServerInterface::class),
48 | ServerOptions::fromArray()
49 | )
50 | );
51 |
52 | $factory = new ResourceServerMiddlewareFactory();
53 | $service = $factory($container);
54 |
55 | $this->assertInstanceOf(ResourceServerMiddleware::class, $service);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Container/AuthorizationServerFactory.php:
--------------------------------------------------------------------------------
1 | get(ClientService::class);
40 |
41 | /** @var ServerOptions $serverOptions */
42 | $serverOptions = $container->get(ServerOptions::class);
43 |
44 | $grants = [];
45 | foreach ($serverOptions->getGrants() as $grant) {
46 | $grants[] = $container->get($grant);
47 | }
48 |
49 | /** @var AccessTokenService $accessTokenService */
50 | $accessTokenService = $container->get(AccessTokenService::class);
51 |
52 | /** @var RefreshTokenService $refreshTokenService */
53 | $refreshTokenService = $container->get(RefreshTokenService::class);
54 |
55 | return new AuthorizationServer($clientService, $grants, $accessTokenService, $refreshTokenService);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Model/AuthorizationCode.php:
--------------------------------------------------------------------------------
1 | redirectUri = $redirectUri ?? '';
52 |
53 | return $token;
54 | }
55 |
56 | public static function reconstitute(array $data): self
57 | {
58 | $token = parent::reconstitute($data);
59 |
60 | $token->redirectUri = $data['redirectUri'];
61 |
62 | return $token;
63 | }
64 |
65 | public function getRedirectUri(): string
66 | {
67 | return $this->redirectUri;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/test/src/Container/AuthorizationRequestMiddlewareFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
40 |
41 | $container
42 | ->expects($this->exactly(2))
43 | ->method('get')
44 | ->withConsecutive([AuthorizationServerInterface::class], [ServerOptions::class])
45 | ->will(
46 | $this->onConsecutiveCalls(
47 | $this->createMock(AuthorizationServerInterface::class),
48 | ServerOptions::fromArray(),
49 | )
50 | );
51 |
52 | $factory = new AuthorizationRequestMiddlewareFactory();
53 | $service = $factory($container);
54 |
55 | $this->assertInstanceOf(AuthorizationRequestMiddleware::class, $service);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/src/Container/RefreshTokenGrantFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
41 |
42 | $container
43 | ->expects($this->exactly(3))
44 | ->method('get')
45 | ->withConsecutive([ServerOptions::class], [AccessTokenService::class], [RefreshTokenService::class])
46 | ->will(
47 | $this->onConsecutiveCalls(
48 | ServerOptions::fromArray(),
49 | $this->createMock(AccessTokenService::class),
50 | $this->createMock(RefreshTokenService::class)
51 | )
52 | );
53 |
54 | $factory = new RefreshTokenGrantFactory();
55 | $service = $factory($container);
56 |
57 | $this->assertInstanceOf(RefreshTokenGrant::class, $service);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Service/AccessTokenService.php:
--------------------------------------------------------------------------------
1 | scopeService->getDefaultScopes();
53 | }
54 |
55 | $this->validateTokenScopes($scopes, $client);
56 |
57 | do {
58 | $token = AccessToken::createNewAccessToken(
59 | $this->serverOptions->getAccessTokenTtl(),
60 | $owner,
61 | $client,
62 | $scopes
63 | );
64 | } while ($this->tokenRepository->tokenExists($token->getToken()));
65 |
66 | return $this->tokenRepository->save($token);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/test/src/Container/AccessTokenServiceFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
41 |
42 | $serverOptions = ServerOptions::fromArray();
43 |
44 | $container
45 | ->expects($this->exactly(3))
46 | ->method('get')
47 | ->withConsecutive([ServerOptions::class], [AccessTokenRepositoryInterface::class], [ScopeService::class])
48 | ->will(
49 | $this->onConsecutiveCalls(
50 | $serverOptions,
51 | $this->createMock(AccessTokenRepositoryInterface::class),
52 | $this->createMock(ScopeService::class),
53 | )
54 | );
55 |
56 | $factory = new AccessTokenServiceFactory();
57 | $service = $factory($container);
58 |
59 | $this->assertInstanceOf(AccessTokenService::class, $service);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/src/Middleware/TokenRequestMiddlewareTest.php:
--------------------------------------------------------------------------------
1 | authorizationServer = $this->createMock(AuthorizationServerInterface::class);
46 | $this->middleware = new TokenRequestMiddleware($this->authorizationServer);
47 | }
48 |
49 | public function testCanHandleTokenRequest(): void
50 | {
51 | $request = $this->createMock(RequestInterface::class);
52 | $handler = $this->createMock(RequestHandlerInterface::class);
53 |
54 | $this->authorizationServer->expects($this->once())
55 | ->method('handleTokenRequest')
56 | ->with($request)
57 | ->willReturn($this->createMock(ResponseInterface::class));
58 |
59 | $this->middleware->process($request, $handler);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Service/RefreshTokenService.php:
--------------------------------------------------------------------------------
1 | scopeService->getDefaultScopes();
53 | }
54 |
55 | $this->validateTokenScopes($scopes, $client);
56 |
57 | do {
58 | $token = RefreshToken::createNewRefreshToken(
59 | $this->serverOptions->getRefreshTokenTtl(),
60 | $owner,
61 | $client,
62 | $scopes
63 | );
64 | } while ($this->tokenRepository->tokenExists($token->getToken()));
65 |
66 | return $this->tokenRepository->save($token);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/test/src/Container/RefreshTokenServiceFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
41 |
42 | $serverOptions = ServerOptions::fromArray();
43 |
44 | $container
45 | ->expects($this->exactly(3))
46 | ->method('get')
47 | ->withConsecutive([ServerOptions::class], [RefreshTokenRepositoryInterface::class], [ScopeService::class])
48 | ->will(
49 | $this->onConsecutiveCalls(
50 | $serverOptions,
51 | $this->createMock(RefreshTokenRepositoryInterface::class),
52 | $this->createMock(ScopeService::class)
53 | )
54 | );
55 |
56 | $factory = new RefreshTokenServiceFactory();
57 | $service = $factory($container);
58 |
59 | $this->assertInstanceOf(RefreshTokenService::class, $service);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/src/Exception/OAuth2ExceptionTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(OAuth2Exception::class, $exception);
41 | $this->assertSame('description', $exception->getMessage());
42 | $this->assertSame($expectedErrorCode, $exception->getCode());
43 | }
44 |
45 | public function dataproviderErrorsCode(): array
46 | {
47 | return [
48 | ['accessDenied', 'access_denied'],
49 | ['invalidRequest', 'invalid_request'],
50 | ['invalidClient', 'invalid_client'],
51 | ['invalidGrant', 'invalid_grant'],
52 | ['invalidScope', 'invalid_scope'],
53 | ['serverError', 'server_error'],
54 | ['temporarilyUnavailable', 'temporarily_unavailable'],
55 | ['unauthorizedClient', 'unauthorized_client'],
56 | ['unsupportedGrantType', 'unsupported_grant_type'],
57 | ['unsupportedResponseType', 'unsupported_response_type'],
58 | ['unsupportedTokenType', 'unsupported_token_type'],
59 | ];
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/src/Middleware/RevocationRequestMiddlewareTest.php:
--------------------------------------------------------------------------------
1 | authorizationServer = $this->createMock(AuthorizationServerInterface::class);
46 | $this->middleware = new RevocationRequestMiddleware($this->authorizationServer);
47 | }
48 |
49 | public function testCanHandleRevocationRequest(): void
50 | {
51 | $request = $this->createMock(RequestInterface::class);
52 | $delegate = $this->createMock(RequestHandlerInterface::class);
53 |
54 | $this->authorizationServer->expects($this->once())
55 | ->method('handleRevocationRequest')
56 | ->with($request)
57 | ->willReturn($this->createMock(ResponseInterface::class));
58 |
59 | $this->middleware->process($request, $delegate);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/src/Container/AuthorizationGrantFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
41 |
42 | $container
43 | ->expects($this->exactly(3))
44 | ->method('get')
45 | ->withConsecutive(
46 | [AuthorizationCodeService::class],
47 | [AccessTokenService::class],
48 | [RefreshTokenService::class]
49 | )
50 | ->will(
51 | $this->onConsecutiveCalls(
52 | $this->createMock(AuthorizationCodeService::class),
53 | $this->createMock(AccessTokenService::class),
54 | $this->createMock(RefreshTokenService::class),
55 | )
56 | );
57 |
58 | $factory = new AuthorizationGrantFactory();
59 | $service = $factory($container);
60 |
61 | $this->assertInstanceOf(AuthorizationGrant::class, $service);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Service/ClientService.php:
--------------------------------------------------------------------------------
1 | clientRepository = $clientRepository;
39 | }
40 |
41 | /**
42 | * Register a new client and generate a strong secret
43 | *
44 | * Please note that the secret must be really kept secret, as it is used for some grant type to
45 | * authorize the client. It is returned as a result of this method, as it's already encrypted
46 | * in the client object
47 | *
48 | * @param array $redirectUris
49 | * @param array $scopes
50 | * @return array [$client, $secret]
51 | */
52 | public function registerClient(string $name, array $redirectUris, array $scopes = []): array
53 | {
54 | do {
55 | $client = Client::createNewClient($name, $redirectUris, $scopes);
56 | } while ($this->clientRepository->idExists($client->getId()));
57 |
58 | $secret = $client->generateSecret();
59 | $client = $this->clientRepository->save($client);
60 |
61 | return [$client, $secret];
62 | }
63 |
64 | /**
65 | * Get the client using its id
66 | *
67 | * @return Client|null
68 | */
69 | public function getClient(string $id)
70 | {
71 | return $this->clientRepository->findById($id);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Grant/AbstractGrant.php:
--------------------------------------------------------------------------------
1 | getOwner();
56 | $scopes = $useRefreshTokenScopes ? $refreshToken->getScopes() : $accessToken->getScopes();
57 |
58 | $responseBody = [
59 | 'access_token' => $accessToken->getToken(),
60 | 'token_type' => 'Bearer',
61 | 'expires_in' => $accessToken->getExpiresIn(),
62 | 'scope' => implode(' ', $scopes),
63 | 'owner_id' => $owner ? $owner->getTokenOwnerId() : null,
64 | ];
65 |
66 | if (null !== $refreshToken) {
67 | $responseBody['refresh_token'] = $refreshToken->getToken();
68 | }
69 |
70 | return new Response\JsonResponse(array_filter($responseBody));
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/test/src/Service/ScopeServiceTest.php:
--------------------------------------------------------------------------------
1 | scopeRepository = $this->createMock(ScopeRepositoryInterface::class);
41 | $this->tokenService = new ScopeService($this->scopeRepository);
42 | }
43 |
44 | public function testCanCreateScope(): void
45 | {
46 | $scope = Scope::createNewScope(1, 'name');
47 | $this->scopeRepository->expects($this->once())
48 | ->method('save')
49 | ->with($scope)
50 | ->willReturn($scope);
51 |
52 | $this->tokenService->createScope($scope);
53 | }
54 |
55 | public function testCanGetAllScopesFromRepository(): void
56 | {
57 | $this->scopeRepository->expects($this->once())
58 | ->method('findAllScopes')
59 | ->with();
60 |
61 | $this->tokenService->getAll();
62 | }
63 |
64 | public function testGetDefaultScopes(): void
65 | {
66 | $this->scopeRepository->expects($this->once())
67 | ->method('findDefaultScopes')
68 | ->with()
69 | ->willReturn([]);
70 |
71 | $this->assertIsArray($this->tokenService->getDefaultScopes());
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/test/src/Container/AuthorizationCodeServiceFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
41 |
42 | $serverOptions = ServerOptions::fromArray();
43 |
44 | $container
45 | ->expects($this->exactly(3))
46 | ->method('get')
47 | ->withConsecutive(
48 | [ServerOptions::class],
49 | [AuthorizationCodeRepositoryInterface::class],
50 | [ScopeService::class]
51 | )
52 | ->will(
53 | $this->onConsecutiveCalls(
54 | $serverOptions,
55 | $this->createMock(AuthorizationCodeRepositoryInterface::class),
56 | $this->createMock(ScopeService::class),
57 | )
58 | );
59 |
60 | $factory = new AuthorizationCodeServiceFactory();
61 | $service = $factory($container);
62 |
63 | $this->assertInstanceOf(AuthorizationCodeService::class, $service);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Service/AuthorizationCodeService.php:
--------------------------------------------------------------------------------
1 | scopeService->getDefaultScopes();
54 | }
55 |
56 | $this->validateTokenScopes($scopes, $client);
57 |
58 | do {
59 | $token = AuthorizationCode::createNewAuthorizationCode(
60 | $this->serverOptions->getAuthorizationCodeTtl(),
61 | $redirectUri,
62 | $owner,
63 | $client,
64 | $scopes
65 | );
66 | } while ($this->tokenRepository->tokenExists($token->getToken()));
67 |
68 | return $this->tokenRepository->save($token);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/AuthorizationServerInterface.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
43 | $serverOptions = ServerOptions::fromArray(['grants' => ['MyGrant']]);
44 |
45 | $container
46 | ->expects($this->exactly(5))
47 | ->method('get')
48 | ->withConsecutive(
49 | [ClientService::class],
50 | [ServerOptions::class],
51 | ['MyGrant'],
52 | [AccessTokenService::class],
53 | [RefreshTokenService::class]
54 | )
55 | ->will(
56 | $this->onConsecutiveCalls(
57 | $this->createMock(ClientService::class),
58 | $serverOptions,
59 | $this->createMock(GrantInterface::class),
60 | $this->createMock(AccessTokenService::class),
61 | $this->createMock(RefreshTokenService::class),
62 | )
63 | );
64 |
65 | $factory = new AuthorizationServerFactory();
66 | $service = $factory($container);
67 |
68 | $this->assertInstanceOf(AuthorizationServer::class, $service);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Grant/GrantInterface.php:
--------------------------------------------------------------------------------
1 | assertEquals($id, $scope->getId());
42 | $this->assertEquals($name, $scope->getName());
43 | $this->assertEquals($description, $scope->getDescription());
44 | $this->assertEquals($isDefault, $scope->isDefault());
45 | }
46 |
47 | public function providerGenerateNewScope(): array
48 | {
49 | return [
50 | [1, 'name', 'description', false],
51 | [1, 'name', 'description', true],
52 | ];
53 | }
54 |
55 | /**
56 | * @dataProvider providerReconstitute
57 | */
58 | public function testReconstitute(array $data): void
59 | {
60 | $scope = Scope::reconstitute($data);
61 |
62 | $this->assertEquals($data['id'], $scope->getId());
63 | $this->assertSame($data['name'], $scope->getName());
64 | $this->assertSame($data['description'], $scope->getDescription());
65 | $this->assertEquals($data['isDefault'], $scope->isDefault());
66 | }
67 |
68 | public function providerReconstitute(): array
69 | {
70 | return [
71 | [
72 | ['id' => 1, 'name' => 'name', 'description' => 'description', 'isDefault' => true],
73 | ['id' => 1, 'name' => 'name', 'description' => null, 'isDefault' => false],
74 | ],
75 | ];
76 | }
77 |
78 | public function testToString(): void
79 | {
80 | $scope = Scope::createNewScope(1, 'name', 'description');
81 |
82 | $this->assertEquals('name', (string) $scope);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Model/Scope.php:
--------------------------------------------------------------------------------
1 | id = $id;
57 | $scope->name = $name;
58 | $scope->description = $description;
59 | $scope->isDefault = $isDefault;
60 |
61 | return $scope;
62 | }
63 |
64 | public static function reconstitute(array $data): Scope
65 | {
66 | $scope = new static();
67 |
68 | $scope->id = $data['id'];
69 | $scope->name = $data['name'];
70 | $scope->description = $data['description'];
71 | $scope->isDefault = $data['isDefault'];
72 |
73 | return $scope;
74 | }
75 |
76 | public function getId(): ?int
77 | {
78 | return $this->id;
79 | }
80 |
81 | /**
82 | * Get the scope's name
83 | */
84 | public function getName(): string
85 | {
86 | return $this->name;
87 | }
88 |
89 | /**
90 | * Get the scope's description
91 | */
92 | public function getDescription(): string
93 | {
94 | return $this->description;
95 | }
96 |
97 | /**
98 | * Is the scope a default scope?
99 | */
100 | public function isDefault(): bool
101 | {
102 | return $this->isDefault;
103 | }
104 |
105 | public function __toString(): string
106 | {
107 | return $this->name;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/test/src/Service/ClientServiceTest.php:
--------------------------------------------------------------------------------
1 | clientRepository = $this->createMock(ClientRepositoryInterface::class);
47 | $this->clientService = new ClientService($this->clientRepository);
48 | }
49 |
50 | public function testCanGetClient(): void
51 | {
52 | $client = Client::reconstitute([
53 | 'id' => 'client_id',
54 | 'name' => 'name',
55 | 'secret' => '',
56 | 'redirectUris' => [],
57 | 'scopes' => [],
58 | ]);
59 |
60 | $this->clientRepository->expects($this->once())
61 | ->method('findById')
62 | ->with('client_id')
63 | ->will($this->returnValue($client));
64 |
65 | $this->assertSame($client, $this->clientService->getClient('client_id'));
66 | }
67 |
68 | public function testRegisterClient(): void
69 | {
70 | $this->clientRepository->expects($this->once())
71 | ->method('idExists')
72 | ->willReturn(false);
73 |
74 | $this->clientRepository->expects($this->once())
75 | ->method('save')
76 | ->will($this->returnArgument(0));
77 |
78 | [$client, $secret] = $this->clientService->registerClient('name', ['http://www.example.com']);
79 |
80 | $this->assertEquals(60, strlen($client->getSecret()));
81 | $this->assertEquals(40, strlen($secret));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Middleware/ResourceServerMiddleware.php:
--------------------------------------------------------------------------------
1 | resourceServer = $resourceServer;
50 | $this->tokenRequestAttribute = $tokenRequestAttribute;
51 | }
52 |
53 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
54 | {
55 | try {
56 | $token = $this->resourceServer->getAccessToken($request);
57 | } catch (InvalidAccessTokenException $exception) {
58 | // If we're here, this means that there was an access token, but it's either expired or invalid. If
59 | // that's the case we must immediately return
60 | return new JsonResponse(
61 | ['error' => $exception->getCode(), 'error_description' => $exception->getMessage()],
62 | 401
63 | );
64 | }
65 |
66 | // Otherwise, if we actually have a token and set it as part of the request attribute for next step
67 | return $handler->handle($request->withAttribute($this->tokenRequestAttribute, $token));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Grant/ClientCredentialsGrant.php:
--------------------------------------------------------------------------------
1 | accessTokenService = $accessTokenService;
57 | }
58 |
59 | /**
60 | * @throws OAuth2Exception (invalid_request).
61 | */
62 | public function createAuthorizationResponse(
63 | ServerRequestInterface $request,
64 | Client $client,
65 | ?TokenOwnerInterface $owner = null
66 | ): ResponseInterface {
67 | throw OAuth2Exception::invalidRequest('Client credentials grant does not support authorization');
68 | }
69 |
70 | /**
71 | * {@inheritdoc}
72 | */
73 | public function createTokenResponse(
74 | ServerRequestInterface $request,
75 | ?Client $client = null,
76 | ?TokenOwnerInterface $owner = null
77 | ): ResponseInterface {
78 | $postParams = $request->getParsedBody();
79 |
80 | // Everything is okey, we can start tokens generation!
81 | $scope = $postParams['scope'] ?? null;
82 | $scopes = is_string($scope) ? explode(' ', $scope) : [];
83 |
84 | /** @var AccessToken $accessToken */
85 | $accessToken = $this->accessTokenService->createToken($owner, $client, $scopes);
86 |
87 | return $this->prepareTokenResponse($accessToken);
88 | }
89 |
90 | /**
91 | * {@inheritdoc}
92 | */
93 | public function allowPublicClients(): bool
94 | {
95 | return false;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/test/src/Options/ServerOptionsTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(120, $options->getAuthorizationCodeTtl());
39 | $this->assertEquals(3600, $options->getAccessTokenTtl());
40 | $this->assertEquals(86400, $options->getRefreshTokenTtl());
41 | $this->assertNull($options->getOwnerCallable());
42 | $this->assertEmpty($options->getGrants());
43 | $this->assertFalse($options->getRotateRefreshTokens());
44 | $this->assertTrue($options->getRevokeRotatedRefreshTokens());
45 | $this->assertEquals('owner', $options->getOwnerRequestAttribute());
46 | $this->assertEquals('oauth_token', $options->getTokenRequestAttribute());
47 | }
48 |
49 | public function testGetters(): void
50 | {
51 | $callable = function () {
52 | };
53 |
54 | $options = ServerOptions::fromArray([
55 | 'authorization_code_ttl' => 300,
56 | 'access_token_ttl' => 3000,
57 | 'refresh_token_ttl' => 30000,
58 | 'rotate_refresh_tokens' => true,
59 | 'revoke_rotated_refresh_tokens' => false,
60 | 'owner_callable' => $callable,
61 | 'grants' => [ClientCredentialsGrant::class],
62 | 'owner_request_attribute' => 'something',
63 | 'token_request_attribute' => 'else',
64 | ]);
65 |
66 | $this->assertEquals(300, $options->getAuthorizationCodeTtl());
67 | $this->assertEquals(3000, $options->getAccessTokenTtl());
68 | $this->assertEquals(30000, $options->getRefreshTokenTtl());
69 | $this->assertEquals(true, $options->getRotateRefreshTokens());
70 | $this->assertEquals(false, $options->getRevokeRotatedRefreshTokens());
71 | $this->assertSame($callable, $options->getOwnerCallable());
72 | $this->assertEquals([ClientCredentialsGrant::class], $options->getGrants());
73 | $this->assertEquals('something', $options->getOwnerRequestAttribute());
74 | $this->assertEquals('else', $options->getTokenRequestAttribute());
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/test/src/Container/PasswordGrantFactoryTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerInterface::class);
41 | $callable = function () {
42 | };
43 | $options = ServerOptions::fromArray(['owner_callable' => $callable]);
44 |
45 | $container
46 | ->expects($this->exactly(3))
47 | ->method('get')
48 | ->withConsecutive([ServerOptions::class], [AccessTokenService::class], [RefreshTokenService::class])
49 | ->will(
50 | $this->onConsecutiveCalls(
51 | $options,
52 | $this->createMock(AccessTokenService::class),
53 | $this->createMock(RefreshTokenService::class)
54 | )
55 | );
56 |
57 | $factory = new PasswordGrantFactory();
58 | $service = $factory($container);
59 |
60 | $this->assertInstanceOf(PasswordGrant::class, $service);
61 | }
62 |
63 | public function testCanCreateFromFactoryOwnerCallableOptionsIsString(): void
64 | {
65 | $container = $this->createMock(ContainerInterface::class);
66 | $callable = function () {
67 | };
68 | $options = ServerOptions::fromArray(['owner_callable' => 'service_name']);
69 |
70 | $container
71 | ->expects($this->exactly(4))
72 | ->method('get')
73 | ->withConsecutive(
74 | [ServerOptions::class],
75 | ['service_name'],
76 | [AccessTokenService::class],
77 | [RefreshTokenService::class]
78 | )
79 | ->will(
80 | $this->onConsecutiveCalls(
81 | $options,
82 | $callable,
83 | $this->createMock(AccessTokenService::class),
84 | $this->createMock(RefreshTokenService::class)
85 | )
86 | );
87 |
88 | $factory = new PasswordGrantFactory();
89 | $service = $factory($container);
90 |
91 | $this->assertInstanceOf(PasswordGrant::class, $service);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/ResourceServer.php:
--------------------------------------------------------------------------------
1 | accessTokenService = $accessTokenService;
50 | }
51 |
52 | /**
53 | * Get the access token
54 | *
55 | * Note that this method will only match tokens that are not expired and match the given scopes (if any).
56 | * If no token is pass, this method will return null, but if a token is given does not exist (ie. has been
57 | * deleted) or is not valid, then it will trigger an exception
58 | *
59 | * @link http://tools.ietf.org/html/rfc6750#page-5
60 | *
61 | * @param array|string|Scope[] $scopes
62 | * @throws InvalidAccessTokenException If given access token is invalid or expired.
63 | */
64 | public function getAccessToken(ServerRequestInterface $request, $scopes = []): ?AccessToken
65 | {
66 | if (! $token = $this->extractAccessToken($request)) {
67 | return null;
68 | }
69 |
70 | $token = $this->accessTokenService->getToken($token);
71 | /** @var AccessToken $token */
72 |
73 | if ($token === null || ! $token->isValid($scopes)) {
74 | throw InvalidAccessTokenException::invalidToken('Access token has expired or has been deleted');
75 | }
76 |
77 | return $token;
78 | }
79 |
80 | /**
81 | * Extract the token either from Authorization header or query params
82 | */
83 | private function extractAccessToken(ServerRequestInterface $request): ?string
84 | {
85 | // The preferred way is using Authorization header
86 | if ($request->hasHeader('Authorization')) {
87 | // Header value is expected to be "Bearer xxx"
88 | $parts = explode(' ', $request->getHeaderLine('Authorization'));
89 |
90 | if (count($parts) < 2) {
91 | return null;
92 | }
93 |
94 | return end($parts);
95 | }
96 |
97 | // Default back to authorization in query param
98 | $queryParams = $request->getQueryParams();
99 |
100 | return $queryParams['access_token'] ?? null;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/test/src/Model/AbstractTokenTest.php:
--------------------------------------------------------------------------------
1 | owner = $this->createMock(TokenOwnerInterface::class);
39 | $this->client = $this->createMock(Client::class);
40 | $this->expiresAt = (new DateTime())->modify('+60 seconds');
41 | $this->scopes = ['somescope', 'otherscope'];
42 |
43 | $this->token = SomeToken::reconstitute([
44 | 'token' => 'a token',
45 | 'expiresAt' => $this->expiresAt,
46 | 'owner' => $this->owner,
47 | 'client' => $this->client,
48 | 'scopes' => $this->scopes,
49 | ]);
50 | }
51 |
52 | public function testMethodGetToken(): void
53 | {
54 | $this->assertSame('a token', $this->token->getToken());
55 | }
56 |
57 | public function testMethodGetOwner(): void
58 | {
59 | $this->assertSame($this->owner, $this->token->getOwner());
60 | }
61 |
62 | public function testMethodGetClient(): void
63 | {
64 | $this->assertSame($this->client, $this->token->getClient());
65 | }
66 |
67 | public function testMethodGetExpiresAt(): void
68 | {
69 | $this->assertSame(
70 | $this->expiresAt->format(DateTime::ATOM),
71 | $this->token->getExpiresAt()->format(DateTime::ATOM)
72 | );
73 | }
74 |
75 | public function testMethodGetExpiresIn(): void
76 | {
77 | $this->assertIsInt($this->token->getExpiresIn());
78 | $this->assertSame(60, $this->token->getExpiresIn());
79 | }
80 |
81 | public function testMethodGetIsExpired(): void
82 | {
83 | $this->assertIsBool($this->token->isExpired());
84 | $this->assertFalse($this->token->isExpired());
85 | }
86 |
87 | public function testMethodGetScopes(): void
88 | {
89 | $this->assertSame($this->scopes, $this->token->getScopes());
90 | }
91 |
92 | public function testMethodMatchScopes(): void
93 | {
94 | $this->assertTrue($this->token->matchScopes($this->scopes));
95 | $this->assertTrue($this->token->matchScopes('somescope'));
96 |
97 | $this->assertFalse($this->token->matchScopes('unknownscope'));
98 | }
99 |
100 | public function testMethodIsValid(): void
101 | {
102 | $this->assertTrue($this->token->isValid($this->scopes));
103 | $this->assertFalse($this->token->isValid('unknownscope'));
104 | }
105 |
106 | public function testMethodIsValidWithExpired(): void
107 | {
108 | // expired
109 | $this->expiresAt = (new DateTime())->modify('-60 seconds');
110 |
111 | $this->token = SomeToken::reconstitute([
112 | 'token' => 'a token',
113 | 'expiresAt' => $this->expiresAt,
114 | 'owner' => $this->owner,
115 | 'client' => $this->client,
116 | 'scopes' => $this->scopes,
117 | ]);
118 |
119 | $this->assertFalse($this->token->isValid('somescope'));
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/config/dependencies.global.php:
--------------------------------------------------------------------------------
1 | [
56 | 'factories' => [
57 | /**
58 | * Middleware
59 | */
60 | AuthorizationRequestMiddleware::class => AuthorizationRequestMiddlewareFactory::class,
61 | RevocationRequestMiddleware::class => RevocationRequestMiddlewareFactory::class,
62 | TokenRequestMiddleware::class => TokenRequestMiddlewareFactory::class,
63 | ResourceServerMiddleware::class => ResourceServerMiddlewareFactory::class,
64 |
65 | /**
66 | * Services
67 | */
68 | AuthorizationServerInterface::class => AuthorizationServerFactory::class,
69 | ResourceServerInterface::class => ResourceServerFactory::class,
70 | ClientService::class => ClientServiceFactory::class,
71 | ScopeService::class => ScopeServiceFactory::class,
72 | AuthorizationCodeService::class => AuthorizationCodeServiceFactory::class,
73 | AccessTokenService::class => AccessTokenServiceFactory::class,
74 | RefreshTokenService::class => RefreshTokenServiceFactory::class,
75 |
76 | /**
77 | * Grant Services
78 | */
79 | ClientCredentialsGrant::class => ClientCredentialsGrantFactory::class,
80 | PasswordGrant::class => PasswordGrantFactory::class,
81 | AuthorizationGrant::class => AuthorizationGrantFactory::class,
82 | RefreshTokenGrant::class => RefreshTokenGrantFactory::class,
83 |
84 | /**
85 | * Utils
86 | */
87 | ServerOptions::class => ServerOptionsFactory::class,
88 | ],
89 | ],
90 | ];
91 |
--------------------------------------------------------------------------------
/test/src/Grant/ClientCredentialsGrantTest.php:
--------------------------------------------------------------------------------
1 | tokenService = $this->createMock(AccessTokenService::class);
59 | $this->grant = new ClientCredentialsGrant($this->tokenService);
60 | }
61 |
62 | public function tearDown(): void
63 | {
64 | Carbon::setTestNow();
65 | }
66 |
67 | public function testAssertDoesNotImplementAuthorization()
68 | {
69 | $this->expectException(OAuth2Exception::class, null, 'invalid_request');
70 | $this->grant->createAuthorizationResponse(
71 | $this->createMock(ServerRequestInterface::class),
72 | Client::createNewClient('id', 'http://www.example.com')
73 | );
74 | }
75 |
76 | public function testCanCreateTokenResponse(): void
77 | {
78 | Carbon::setTestNow('1970-01-01 02:46:40');
79 |
80 | $request = $this->createMock(ServerRequestInterface::class);
81 |
82 | $client = Client::createNewClient('name', 'http://www.example.com');
83 | $owner = $this->createMock(TokenOwnerInterface::class);
84 | $owner->expects($this->once())->method('getTokenOwnerId')->will($this->returnValue(1));
85 |
86 | $token = AccessToken::reconstitute([
87 | 'token' => 'azerty',
88 | 'owner' => $owner,
89 | 'client' => null,
90 | 'expiresAt' => (new DateTimeImmutable('@10000'))->add(new DateInterval('PT1H')),
91 | 'scopes' => [],
92 | ]);
93 |
94 | $this->tokenService->expects($this->once())->method('createToken')->will($this->returnValue($token));
95 |
96 | $response = $this->grant->createTokenResponse($request, $client, $owner);
97 |
98 | $body = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
99 |
100 | $this->assertEquals('azerty', $body['access_token']);
101 | $this->assertEquals('Bearer', $body['token_type']);
102 | $this->assertEquals(3600, $body['expires_in']);
103 | $this->assertEquals(1, $body['owner_id']);
104 | }
105 |
106 | public function testMethodGetType(): void
107 | {
108 | $this->assertSame('client_credentials', $this->grant->getType());
109 | }
110 |
111 | public function testMethodGetResponseType(): void
112 | {
113 | $this->assertSame('', $this->grant->getResponseType());
114 | }
115 |
116 | public function testMethodAllowPublicClients(): void
117 | {
118 | $this->assertFalse($this->grant->allowPublicClients());
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Service/AbstractTokenService.php:
--------------------------------------------------------------------------------
1 | tokenRepository = $tokenRepository;
60 | $this->scopeService = $scopeService;
61 | $this->serverOptions = $serverOptions;
62 | }
63 |
64 | /**
65 | * Get a token using its identifier (the token itself)
66 | */
67 | public function getToken(string $token): ?AbstractToken
68 | {
69 | /** @var AbstractToken $tokenFromDb */
70 | $tokenFromDb = $this->tokenRepository->findByToken($token);
71 |
72 | // Because the collation is most often case insensitive, we need to add a check here to ensure
73 | // that the token matches case
74 | if (! $tokenFromDb || ! hash_equals($tokenFromDb->getToken(), $token)) {
75 | return null;
76 | }
77 |
78 | return $tokenFromDb;
79 | }
80 |
81 | /**
82 | * Remove the abstract token from the underlying storage
83 | */
84 | public function deleteToken(AbstractToken $token): void
85 | {
86 | $this->tokenRepository->deleteToken($token);
87 | }
88 |
89 | public function purgeExpiredTokens(): void
90 | {
91 | $this->tokenRepository->purgeExpiredTokens();
92 | }
93 |
94 | /**
95 | * Validate the token scopes against the registered scope
96 | *
97 | * @param string[]|Scope[] $scopes
98 | * @param Client|null $client
99 | * @throws OAuth2Exception (invalid_scope) When one or more of the given scopes where not registered.
100 | */
101 | public function validateTokenScopes(array $scopes, $client = null): void
102 | {
103 | $scopes = array_map(fn($scope) => (string) $scope, $scopes);
104 |
105 | $registeredScopes = $this->scopeService->getAll();
106 |
107 | $registeredScopes = array_map(fn($scope) => (string) $scope, $registeredScopes);
108 |
109 | $diff = array_diff($scopes, $registeredScopes);
110 |
111 | if (count($diff) > 0) {
112 | throw OAuth2Exception::invalidScope(sprintf(
113 | 'Some scope(s) do not exist: %s',
114 | implode(', ', $diff)
115 | ));
116 | }
117 |
118 | if (! $client) {
119 | return;
120 | }
121 |
122 | $clientScopes = $client->getScopes();
123 |
124 | $diff = array_diff($scopes, $clientScopes);
125 |
126 | if (count($diff) > 0) {
127 | throw OAuth2Exception::invalidScope(sprintf(
128 | 'Some scope(s) are not assigned to client: %s',
129 | implode(', ', $diff)
130 | ));
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Exception/OAuth2Exception.php:
--------------------------------------------------------------------------------
1 | code = $code;
45 | }
46 |
47 | /**
48 | * @todo Explain when this excpetion is applicable
49 | */
50 | public static function accessDenied(string $description): OAuth2Exception
51 | {
52 | return new self($description, 'access_denied');
53 | }
54 |
55 | /**
56 | * @todo Explain when this excpetion is applicable
57 | */
58 | public static function invalidRequest(string $description): OAuth2Exception
59 | {
60 | return new self($description, 'invalid_request');
61 | }
62 |
63 | /**
64 | * @todo Explain when this excpetion is applicable
65 | */
66 | public static function invalidClient(string $description): OAuth2Exception
67 | {
68 | return new self($description, 'invalid_client');
69 | }
70 |
71 | /**
72 | * @todo Explain when this excpetion is applicable
73 | */
74 | public static function invalidGrant(string $description): OAuth2Exception
75 | {
76 | return new self($description, 'invalid_grant');
77 | }
78 |
79 | /**
80 | * @todo Explain when this excpetion is applicable
81 | */
82 | public static function invalidScope(string $description): OAuth2Exception
83 | {
84 | return new self($description, 'invalid_scope');
85 | }
86 |
87 | /**
88 | * @todo Explain when this excpetion is applicable
89 | */
90 | public static function serverError(string $description): OAuth2Exception
91 | {
92 | return new self($description, 'server_error');
93 | }
94 |
95 | /**
96 | * @todo Explain when this excpetion is applicable
97 | */
98 | public static function temporarilyUnavailable(string $description): OAuth2Exception
99 | {
100 | return new self($description, 'temporarily_unavailable');
101 | }
102 |
103 | /**
104 | * @todo Explain when this excpetion is applicable
105 | */
106 | public static function unauthorizedClient(string $description): OAuth2Exception
107 | {
108 | return new self($description, 'unauthorized_client');
109 | }
110 |
111 | /**
112 | * @todo Explain when this excpetion is applicable
113 | */
114 | public static function unsupportedGrantType(string $description): OAuth2Exception
115 | {
116 | return new self($description, 'unsupported_grant_type');
117 | }
118 |
119 | /**
120 | * @todo Explain when this excpetion is applicable
121 | */
122 | public static function unsupportedResponseType(string $description): OAuth2Exception
123 | {
124 | return new self($description, 'unsupported_response_type');
125 | }
126 |
127 | /**
128 | * @link https://tools.ietf.org/html/rfc7009#section-2.2.1
129 | */
130 | public static function unsupportedTokenType(string $description): OAuth2Exception
131 | {
132 | return new self($description, 'unsupported_token_type');
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/test/src/Grant/AbstractGrantTest.php:
--------------------------------------------------------------------------------
1 | getMockForAbstractClass(AbstractGrant::class);
40 | $this->assertSame('', $abstractGrant->getType());
41 | }
42 |
43 | public function testMethodGetResponseType(): void
44 | {
45 | $abstractGrant = $this->getMockForAbstractClass(AbstractGrant::class);
46 | $this->assertSame('', $abstractGrant->getResponseType());
47 | }
48 |
49 | /**
50 | * @dataProvider dpMethodPrepareTokenResponse
51 | */
52 | public function testMethodPrepareTokenResponse(
53 | ?RefreshToken $refreshToken,
54 | bool $useRefreshTokenScopes,
55 | ?TokenOwnerInterface $getOwner
56 | ): void {
57 | $abstractGrant = $this->getMockForAbstractClass(AbstractGrant::class);
58 | $accessToken = $this->createMock(AccessToken::class);
59 |
60 | $accessToken->expects($this->once())->method('getOwner')->willReturn($getOwner);
61 | $accessToken->expects($this->once())->method('getExpiresIn');
62 |
63 | if ($getOwner) {
64 | $getOwner->expects($this->once())->method('getTokenOwnerId');
65 | }
66 |
67 | if ($useRefreshTokenScopes) {
68 | $refreshToken->expects($this->once())->method('getScopes');
69 | } else {
70 | $accessToken->expects($this->once())->method('getScopes');
71 | }
72 |
73 | if (null !== $refreshToken) {
74 | $refreshToken->expects($this->once())->method('getToken');
75 | }
76 |
77 | // calling protected method from abstract token scope
78 | $protectedBound = (fn ($accessToken, $refreshToken, $useRefreshTokenScopes)
79 | => $this->prepareTokenResponse($accessToken, $refreshToken, $useRefreshTokenScopes))
80 | ->bindTo($abstractGrant, $abstractGrant);
81 |
82 | $this->assertInstanceOf(
83 | ResponseInterface::class,
84 | $protectedBound($accessToken, $refreshToken, $useRefreshTokenScopes)
85 | );
86 | }
87 |
88 | public function dpMethodPrepareTokenResponse(): array
89 | {
90 | return [
91 | [
92 | $this->createMock(RefreshToken::class),
93 | true,
94 | $this->createMock(TokenOwnerInterface::class),
95 | ],
96 | [
97 | $this->createMock(RefreshToken::class),
98 | false,
99 | $this->createMock(TokenOwnerInterface::class),
100 | ],
101 | [
102 | null,
103 | false,
104 | $this->createMock(TokenOwnerInterface::class),
105 | ],
106 | [
107 | $this->createMock(RefreshToken::class),
108 | true,
109 | null,
110 | ],
111 | [
112 | $this->createMock(RefreshToken::class),
113 | false,
114 | null,
115 | ],
116 | [
117 | null,
118 | false,
119 | null,
120 | ],
121 | ];
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/test/src/Middleware/ResourceServerMiddlewareTest.php:
--------------------------------------------------------------------------------
1 | createMock(ResourceServer::class);
43 | $middleware = new ResourceServerMiddleware($resourceServer, 'oauth_token');
44 | $accessToken = $this->createMock(AccessToken::class);
45 | $request = $this->createMock(RequestInterface::class);
46 | $response = $this->createMock(ResponseInterface::class);
47 | $handler = $this->createMock(RequestHandlerInterface::class);
48 |
49 | $handler->expects($this->once())
50 | ->method('handle')
51 | ->with($request)
52 | ->willReturn($response);
53 |
54 | $resourceServer->expects($this->once())
55 | ->method('getAccessToken')
56 | ->with($request)
57 | ->willReturn($accessToken);
58 |
59 | $request->expects($this->once())
60 | ->method('withAttribute')
61 | ->with(
62 | 'oauth_token',
63 | $accessToken
64 | )
65 | ->willReturn($request);
66 |
67 | $middleware->process($request, $handler);
68 | }
69 |
70 | public function testWillGetAccessTokenWithNullAsResult(): void
71 | {
72 | $resourceServer = $this->createMock(ResourceServer::class);
73 | $middleware = new ResourceServerMiddleware($resourceServer, 'oauth_token');
74 | $accessToken = null;
75 | $request = $this->createMock(RequestInterface::class);
76 | $response = $this->createMock(ResponseInterface::class);
77 | $handler = $this->createMock(RequestHandlerInterface::class);
78 |
79 | $handler->expects($this->once())
80 | ->method('handle')
81 | ->with($request)
82 | ->willReturn($response);
83 |
84 | $resourceServer->expects($this->once())
85 | ->method('getAccessToken')
86 | ->with($request)
87 | ->willReturn($accessToken);
88 |
89 | $request->expects($this->once())
90 | ->method('withAttribute')
91 | ->with(
92 | 'oauth_token',
93 | $accessToken
94 | )
95 | ->willReturn($request);
96 |
97 | $result = $middleware->process($request, $handler);
98 | }
99 |
100 | public function testWillCallGetAccessTokenWithException(): void
101 | {
102 | $resourceServer = $this->createMock(ResourceServer::class);
103 | $middleware = new ResourceServerMiddleware($resourceServer, 'oauth_token');
104 | $request = $this->createMock(RequestInterface::class);
105 | $handler = $this->createMock(RequestHandlerInterface::class);
106 |
107 | $resourceServer->expects($this->once())
108 | ->method('getAccessToken')
109 | ->with($request)
110 | ->willThrowException(InvalidAccessTokenException::invalidToken('error message'));
111 |
112 | $result = $middleware->process($request, $handler);
113 |
114 | $this->assertInstanceOf(JsonResponse::class, $result);
115 |
116 | $this->assertSame(401, $result->getStatusCode());
117 | $this->assertSame('{"error":"invalid_token","error_description":"error message"}', (string) $result->getBody());
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Grant/RefreshTokenGrant.php:
--------------------------------------------------------------------------------
1 | accessTokenService = $accessTokenService;
57 | $this->refreshTokenService = $refreshTokenService;
58 | $this->serverOptions = $serverOptions;
59 | }
60 |
61 | /**
62 | * @throws OAuth2Exception (invalid_request).
63 | */
64 | public function createAuthorizationResponse(
65 | ServerRequestInterface $request,
66 | Client $client,
67 | ?TokenOwnerInterface $owner = null
68 | ): ResponseInterface {
69 | throw OAuth2Exception::invalidRequest('Refresh token grant does not support authorization');
70 | }
71 |
72 | /**
73 | * {@inheritdoc}
74 | */
75 | public function createTokenResponse(
76 | ServerRequestInterface $request,
77 | ?Client $client = null,
78 | ?TokenOwnerInterface $owner = null
79 | ): ResponseInterface {
80 | $postParams = $request->getParsedBody();
81 |
82 | $refreshToken = $postParams['refresh_token'] ?? null;
83 |
84 | if (null === $refreshToken) {
85 | throw OAuth2Exception::invalidRequest('Refresh token is missing');
86 | }
87 |
88 | // We can fetch the actual token, and validate it
89 | /** @var RefreshToken $refreshToken */
90 | $refreshToken = $this->refreshTokenService->getToken((string) $refreshToken);
91 |
92 | if (null === $refreshToken || $refreshToken->isExpired()) {
93 | throw OAuth2Exception::invalidGrant('Refresh token is expired');
94 | }
95 |
96 | // We can now create a new access token! First, we need to make some checks on the asked scopes,
97 | // because according to the spec, a refresh token can create an access token with an equal or lesser
98 | // scope, but not more
99 | $scope = $postParams['scope'] ?? null;
100 | $scopes = is_string($scope) ? explode(' ', $scope) : $refreshToken->getScopes();
101 |
102 | if (! $refreshToken->matchScopes($scopes)) {
103 | throw OAuth2Exception::invalidScope(
104 | 'The scope of the new access token exceeds the scope(s) of the refresh token'
105 | );
106 | }
107 |
108 | $owner = $refreshToken->getOwner();
109 | $accessToken = $this->accessTokenService->createToken($owner, $client, $scopes);
110 |
111 | // We may want to revoke the old refresh token
112 | if ($this->serverOptions->getRotateRefreshTokens()) {
113 | if ($this->serverOptions->getRevokeRotatedRefreshTokens()) {
114 | $this->refreshTokenService->deleteToken($refreshToken);
115 | }
116 |
117 | $refreshToken = $this->refreshTokenService->createToken($owner, $client, $scopes);
118 | }
119 |
120 | // We can generate the response!
121 | return $this->prepareTokenResponse($accessToken, $refreshToken, true);
122 | }
123 |
124 | public function allowPublicClients(): bool
125 | {
126 | return true;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## dev-master
4 |
5 | ## v0.8.0-beta3
6 |
7 | * Implements [PSR-15](https://github.com/php-fig/fig-standards/tree/master/proposed/http-middleware)
8 |
9 | ## v0.8.0-beta2
10 |
11 | * When an token can't be found the returned error response by the resource server middleware is now in a similar format to other errors. This might BC if your client depends on the error key in the message.
12 | * Added an server option so the request attribute for tokens can be configured
13 |
14 | ## v0.8.0-beta1
15 |
16 | BC! Pre release of a completely rewritten library. It focusses on core OAuth2 functionality and has been decoupled from persistence. If you still need the previous implementation - which is considered EOL - see the [legacy-0.7](https://github.com/zf-fr/zfr-oauth2-server/tree/legacy-0.7) branch
17 |
18 | * PHP7+ only
19 | * 100% test coverage
20 | * Uses [Zend\Diactoros](https://github.com/zendframework/zend-diactoros) to generate [PSR-7 (Http Message)](https://github.com/php-fig/http-message) implementation.
21 | * Uses [PSR-11 (Container)](https://github.com/php-fig/container) for dependency injection containers.
22 | * Eventing has been removed
23 | * Persistence has been decoupled, see our doctrine integration [ZfrOAuth2ServerDoctrine](https://github.com/zf-fr/zfr-oauth2-server-doctrine)
24 | * Provides 5 Services
25 | * ZfrOAuth2\Server\Service\AccessTokenService
26 | * ZfrOAuth2\Server\Service\AuthorizationCodeService
27 | * ZfrOAuth2\Server\Service\ClientService
28 | * ZfrOAuth2\Server\Service\RefreshTokenService
29 | * ZfrOAuth2\Server\Service\ScopeService
30 | * Provides 4 PSR7 Middleware's which are really nice but optional
31 | * ZfrOAuth2\Server\AuthorizationServerMiddleware
32 | * ZfrOAuth2\Server\ResourceServerMiddleware
33 | * ZfrOAuth2\Server\RevocationRequestMiddleware
34 | * ZfrOAuth2\Server\TokenRequestMiddleware
35 |
36 | ## v0.7.1
37 |
38 | * Now properly triggers an `EVENT_CODE_CREATED` event instead of `EVENT_CODE_FAILED` when response is between 200 and 399 (previously,
39 | as 302 Redirect used to trigger a failed event, although it created an authorization code).
40 |
41 | ## v0.7.0
42 |
43 | * [BC] PHP minimum version has been bumped to 5.5. As a consequence, Zend\Crypt dependency has been removed as some of
44 | features are built-in into PHP 5.5.
45 |
46 | * [BC] Instead of Zend\Http requests and responses, the module now uses PSR7 requests and responses, for increased
47 | compatibility. If you are using the ZF2 module, this should be completely transparent to you.
48 |
49 | * [BC] Contrary to Zend\Http requests and responses, PSR7 are stateless. If you are using events to modify the response,
50 | you will need to use a different way.
51 |
52 | In ZfrOAuth2Server 0.6:
53 |
54 | ```php
55 | public function tokenCreated(TokenEvent $event)
56 | {
57 | // We can log the access token
58 | $accessToken = $event->getAccessToken();
59 | // ...
60 |
61 | // Or we can alter the response body, if we need to
62 | $body = $event->getResponseBody();
63 | $body['custom_field'] = 'bar';
64 |
65 | // Update the body
66 | $event->setResponseBody($body);
67 | }
68 | ```
69 |
70 | In ZfrOAuth2Server 0.7+:
71 |
72 | ```php
73 | public function tokenCreated(TokenEvent $event)
74 | {
75 | // Get the response
76 | $response = $event->getResponse();
77 |
78 | // ...
79 |
80 | // Response is a PSR-7 compliant response, so you modify it
81 | $response = $response->withHeader(...);
82 |
83 | // Do not forget to set back the response, as PSR-7 are immutable
84 | $event->setResponse($response);
85 | }
86 | ```
87 |
88 | * Interfaces for ResourceServer and AuthorizationServer has been added, for easier testing.
89 |
90 | ## v0.6.0
91 |
92 | * In previous versions, ZfrOAuth2 would trigger an "InvalidAccessTokenException" if you'd try to call the `getToken`
93 | when no token was specified in either Authorization header or query param. Now, ZfrOAuth2 will simply return null
94 | (because no token was explicitly set). However, this exception will be trigger IF an access token is indeed given, but
95 | does not exist in your database, is expired or does not match scopes.
96 |
97 | ## v0.5.0
98 |
99 | * Support for token revocation by implementing [RFC7009 specification](https://tools.ietf.org/html/rfc7009)
100 |
101 | ## v0.4.0
102 |
103 | * Allow multiple redirect URI for client (there is a minor table schema change, as a consequence)
104 | * Fix a potential security issue by being more restrictive on the redirect URI when creating an authorization code. Now,
105 | if someone send a custom redirect_uri in the query params, the OAuth2 server will first check if the given redirect URI is
106 | in the list of the authorized redirect URIs by the client. If that's not the case, an InvalidRequest exception will be
107 | returned, and no authorization code will be generated.
108 |
109 | ## v0.3.0
110 |
111 | * Add support for the ZF2 event manager. You can now attach listeners that are called whenever a new authorization code is
112 | created or failed, or when a new access token is created or failed.
113 |
114 | ## v0.2.0
115 |
116 | * [BC] The `isRequestValid` from the ResourceServer is now gone in favour of a simpler approach: you just need to call
117 | the `getAccessToken` from the ResourceServer (with optional scopes), and null will be returned if the token is either expired, does
118 | not exist or does not match given scopes.
119 |
120 | ## v0.1.1
121 |
122 | * Tokens do not contain \ and / characters anymore (as it can lead to problems when the token is passed as a query param).
123 |
124 | ## v0.1.0
125 |
126 | * First release!
127 |
--------------------------------------------------------------------------------
/test/src/Model/AuthorizationCodeTest.php:
--------------------------------------------------------------------------------
1 | assertSame($redirectUri, $authorizationCode->getRedirectUri());
44 | } else {
45 | $this->assertEmpty($authorizationCode->getRedirectUri());
46 | }
47 | }
48 |
49 | public function providerGenerateNewAuthorizationCode(): array
50 | {
51 | return [
52 | [''],
53 | ['http://www.example.com'],
54 | [null],
55 | ];
56 | }
57 |
58 | /**
59 | * @dataProvider providerReconstitute
60 | */
61 | public function testReconstitute(array $data)
62 | {
63 | /** @var AuthorizationCode $authorizationCode */
64 | $authorizationCode = AuthorizationCode::reconstitute($data);
65 |
66 | $this->assertSame($data['redirectUri'], $authorizationCode->getRedirectUri());
67 | }
68 |
69 | public function providerReconstitute(): array
70 | {
71 | return [
72 | [
73 | [
74 | 'token' => 'token',
75 | 'owner' => null,
76 | 'client' => null,
77 | 'expiresAt' => null,
78 | 'scopes' => [],
79 | 'redirectUri' => 'http://www.example.com',
80 | ],
81 | ],
82 | [
83 | [
84 | 'token' => 'token',
85 | 'owner' => null,
86 | 'client' => null,
87 | 'expiresAt' => null,
88 | 'scopes' => [],
89 | 'redirectUri' => '',
90 | ],
91 | ],
92 | ];
93 | }
94 |
95 | public function testCalculateExpiresIn()
96 | {
97 | $authorizationCode = AuthorizationCode::createNewAuthorizationCode(60);
98 |
99 | $this->assertFalse($authorizationCode->isExpired());
100 | $this->assertEquals(60, $authorizationCode->getExpiresIn());
101 | }
102 |
103 | public function testCanCheckIfATokenIsExpired()
104 | {
105 | $authorizationCode = AuthorizationCode::createNewAuthorizationCode(-60);
106 |
107 | $this->assertTrue($authorizationCode->isExpired());
108 | }
109 |
110 | public function testSupportLongLiveToken()
111 | {
112 | $authorizationCode = AuthorizationCode::createNewAuthorizationCode(60);
113 | $this->assertFalse($authorizationCode->isExpired());
114 | }
115 |
116 | public function testIsValid()
117 | {
118 | $authorizationCode = AuthorizationCode::createNewAuthorizationCode(
119 | 60,
120 | 'http://www.example.com',
121 | null,
122 | null,
123 | ['read', 'write']
124 | );
125 | $this->assertTrue($authorizationCode->isValid('read'));
126 |
127 | $authorizationCode = AuthorizationCode::createNewAuthorizationCode(
128 | -60,
129 | 'http://www.example.com',
130 | null,
131 | null,
132 | ['read', 'write']
133 | );
134 | $this->assertFalse($authorizationCode->isValid('read'));
135 |
136 | $authorizationCode = AuthorizationCode::createNewAuthorizationCode(
137 | 60,
138 | 'http://www.example.com',
139 | null,
140 | null,
141 | ['read', 'write']
142 | );
143 | $this->assertFalse($authorizationCode->isValid('delete'));
144 | }
145 |
146 | /**
147 | * @todo I don't get this check
148 | */
149 | public function testDoNotSupportLongLiveToken()
150 | {
151 | $authorizationCode = AuthorizationCode::createNewAuthorizationCode(
152 | 0,
153 | 'http://www.example.com',
154 | null,
155 | null,
156 | ['read', 'write']
157 | );
158 | $this->assertTrue($authorizationCode->isExpired());
159 | }
160 | }
161 |
--------------------------------------------------------------------------------