├── .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 | --------------------------------------------------------------------------------