├── COPYRIGHT.md ├── LICENSE.md ├── README.md ├── bin └── generate-oauth2-keys ├── composer.json ├── config └── oauth2.php ├── data ├── .gitignore ├── oauth2.sql └── oauth2_test.sql └── src ├── AuthorizationHandler.php ├── AuthorizationHandlerFactory.php ├── AuthorizationMiddleware.php ├── AuthorizationMiddlewareFactory.php ├── AuthorizationServerFactory.php ├── ConfigProvider.php ├── ConfigTrait.php ├── CryptKeyTrait.php ├── Entity ├── AccessTokenEntity.php ├── AuthCodeEntity.php ├── ClientEntity.php ├── RefreshTokenEntity.php ├── RevokableTrait.php ├── ScopeEntity.php ├── TimestampableTrait.php └── UserEntity.php ├── Exception ├── ExceptionInterface.php ├── InvalidConfigException.php └── RuntimeException.php ├── Grant ├── AuthCodeGrantFactory.php ├── ClientCredentialsGrantFactory.php ├── ImplicitGrantFactory.php ├── PasswordGrantFactory.php └── RefreshTokenGrantFactory.php ├── OAuth2Adapter.php ├── OAuth2AdapterFactory.php ├── Psr17ResponseFactoryTrait.php ├── Repository └── Pdo │ ├── AbstractRepository.php │ ├── AccessTokenRepository.php │ ├── AccessTokenRepositoryFactory.php │ ├── AuthCodeRepository.php │ ├── AuthCodeRepositoryFactory.php │ ├── ClientRepository.php │ ├── ClientRepositoryFactory.php │ ├── PdoService.php │ ├── PdoServiceFactory.php │ ├── RefreshTokenRepository.php │ ├── RefreshTokenRepositoryFactory.php │ ├── ScopeRepository.php │ ├── ScopeRepositoryFactory.php │ ├── UserRepository.php │ └── UserRepositoryFactory.php ├── RepositoryTrait.php ├── ResourceServerFactory.php ├── Response └── CallableResponseFactoryDecorator.php ├── TokenEndpointHandler.php └── TokenEndpointHandlerFactory.php /COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | - Neither the name of Laminas Foundation nor the names of its contributors may 14 | be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 server middleware for Mezzio and PSR-7 applications 2 | 3 | [![Build Status](https://github.com/mezzio/mezzio-authentication-oauth2/workflows/Continuous%20Integration/badge.svg)](https://github.com/mezzio/mezzio-authentication-oauth2/actions?query=workflow%3A"Continuous+Integration") 4 | 5 | > ## 🇷🇺 Русским гражданам 6 | > 7 | > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм. 8 | > 9 | > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую. 10 | > 11 | > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!" 12 | > 13 | > ## 🇺🇸 To Citizens of Russia 14 | > 15 | > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism. 16 | > 17 | > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences. 18 | > 19 | > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!" 20 | 21 | Laminas-mezzio-authentication-oauth2 is middleware for [Mezzio](https://github.com/mezzio/mezzio) 22 | and [PSR-7](http://www.php-fig.org/psr/psr-7/) applications providing an OAuth2 23 | server for authentication. 24 | 25 | This library uses the [league/oauth2-server](https://oauth2.thephpleague.com/) 26 | package for implementing the OAuth2 server. It supports all the following grant 27 | types: 28 | 29 | - client credentials; 30 | - password; 31 | - authorization code; 32 | - implicit; 33 | - refresh token; 34 | 35 | ## Installation 36 | 37 | You can install the *mezzio-authentication-oauth2* library with 38 | composer: 39 | 40 | ```bash 41 | $ composer require mezzio/mezzio-authentication-oauth2 42 | ``` 43 | 44 | ## Documentation 45 | 46 | Browse the documentation online at https://docs.mezzio.dev/mezzio-authentication-oauth2/ 47 | 48 | ## Support 49 | 50 | - [Issues](https://github.com/mezzio/mezzio-authentication-oauth2/issues/) 51 | - [Chat](https://laminas.dev/chat/) 52 | - [Forum](https://discourse.laminas.dev/) 53 | -------------------------------------------------------------------------------- /bin/generate-oauth2-keys: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $bits = 2048, 63 | 'private_key_type' => OPENSSL_KEYTYPE_RSA, 64 | ]; 65 | 66 | printf('Using %d bits to generate key of type RSA' . "\n\n", $bits); 67 | 68 | // Private key 69 | $res = openssl_pkey_new($config); 70 | 71 | if (!(is_resource($res) || (is_object($res) && $res instanceof OpenSSLAsymmetricKey))) { 72 | fwrite(STDERR, 'Failed to create private key.' . PHP_EOL); 73 | fwrite(STDERR, 'Check your openssl extension settings.' . PHP_EOL); 74 | exit(1); 75 | } 76 | 77 | openssl_pkey_export($res, $privateKey); 78 | file_put_contents($filePrivateKey, $privateKey); 79 | printf("Private key stored in:\n%s\n", $filePrivateKey); 80 | 81 | // Public key 82 | $publicKey = openssl_pkey_get_details($res); 83 | file_put_contents($filePublicKey, $publicKey['key']); 84 | printf("Public key stored in:\n%s\n", $filePublicKey); 85 | 86 | // Encryption key 87 | $encKey = base64_encode(random_bytes(32)); 88 | file_put_contents($fileEncryptionKey, sprintf(" getcwd() . '/data/oauth/private.key', 25 | 'public_key' => getcwd() . '/data/oauth/public.key', 26 | 'access_token_expire' => 'P1D', // 1 day in DateInterval format 27 | 'refresh_token_expire' => 'P1M', // 1 month in DateInterval format 28 | 'auth_code_expire' => 'PT10M', // 10 minutes in DateInterval format 29 | 'pdo' => [ 30 | 'dsn' => '', 31 | 'username' => '', 32 | 'password' => '', 33 | ], 34 | 35 | // Set value to null to disable a grant 36 | 'grants' => [ 37 | ClientCredentialsGrant::class => ClientCredentialsGrant::class, 38 | PasswordGrant::class => PasswordGrant::class, 39 | AuthCodeGrant::class => AuthCodeGrant::class, 40 | ImplicitGrant::class => ImplicitGrant::class, 41 | RefreshTokenGrant::class => RefreshTokenGrant::class, 42 | ], 43 | ]; 44 | 45 | // Conditionally include the encryption_key config setting, based on presence of file. 46 | $encryptionKeyFile = getcwd() . '/data/oauth/encryption.key'; 47 | if (is_readable($encryptionKeyFile)) { 48 | $config['encryption_key'] = require $encryptionKeyFile; 49 | } 50 | 51 | return $config; 52 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | *.sqlite 3 | -------------------------------------------------------------------------------- /data/oauth2.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Table structure for table `oauth_access_tokens` 3 | -- 4 | 5 | CREATE TABLE `oauth_access_tokens` ( 6 | `id` varchar(100) PRIMARY KEY NOT NULL, 7 | `user_id` int(10) DEFAULT NULL, 8 | `client_id` int(10) NOT NULL, 9 | `name` varchar(255) DEFAULT NULL, 10 | `scopes` text, 11 | `revoked` tinyint(1) NOT NULL DEFAULT '0', 12 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | `updated_at` datetime DEFAULT NULL, 14 | `expires_at` datetime NOT NULL 15 | ); 16 | 17 | CREATE INDEX `IDX_CA42527CA76ED39519EB6921BDA26CCD` ON oauth_access_tokens (`user_id`,`client_id`); 18 | CREATE INDEX `IDX_CA42527CA76ED395` ON oauth_access_tokens (`user_id`); 19 | CREATE INDEX `IDX_CA42527C19EB6921` ON oauth_access_tokens (`client_id`); 20 | 21 | -- 22 | -- Table structure for table `oauth_auth_codes` 23 | -- 24 | 25 | CREATE TABLE `oauth_auth_codes` ( 26 | `id` varchar(100) PRIMARY KEY NOT NULL, 27 | `user_id` int(10) DEFAULT NULL, 28 | `client_id` int(10) NOT NULL, 29 | `scopes` text, 30 | `revoked` tinyint(1) NOT NULL DEFAULT '0', 31 | `expires_at` datetime DEFAULT NULL 32 | ); 33 | 34 | CREATE INDEX `IDX_BB493F83A76ED395` ON oauth_auth_codes (`user_id`); 35 | CREATE INDEX `IDX_BB493F8319EB6921` ON oauth_auth_codes (`client_id`); 36 | 37 | -- 38 | -- Table structure for table `oauth_clients` 39 | -- 40 | 41 | CREATE TABLE `oauth_clients` ( 42 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 43 | `user_id` int(10) DEFAULT NULL, 44 | `name` varchar(100) NOT NULL, 45 | `secret` varchar(100) DEFAULT NULL, 46 | `redirect` varchar(255) DEFAULT NULL, 47 | `personal_access_client` tinyint(1) DEFAULT NULL, 48 | `password_client` tinyint(1) DEFAULT NULL, 49 | `revoked` tinyint(1) DEFAULT NULL, 50 | `is_confidential` tinyint(1) NOT NULL DEFAULT '0', 51 | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, 52 | `updated_at` datetime DEFAULT NULL 53 | ); 54 | 55 | CREATE INDEX `IDX_13CE81015E237E06A76ED395BDA26CCD` ON oauth_clients (`name`,`user_id`); 56 | CREATE INDEX `IDX_13CE8101A76ED395` ON oauth_clients (`user_id`); 57 | 58 | -- 59 | -- Table structure for table `oauth_refresh_tokens` 60 | -- 61 | 62 | CREATE TABLE `oauth_refresh_tokens` ( 63 | `id` varchar(100) PRIMARY KEY NOT NULL, 64 | `access_token_id` varchar(100) NOT NULL, 65 | `revoked` tinyint(1) NOT NULL DEFAULT '0', 66 | `expires_at` datetime NOT NULL 67 | ); 68 | 69 | CREATE INDEX `IDX_5AB6872CCB2688BDA26CCD` ON oauth_refresh_tokens (`access_token_id`); 70 | 71 | -- 72 | -- Table structure for table `oauth_scopes` 73 | -- 74 | 75 | CREATE TABLE `oauth_scopes` ( 76 | `id` varchar(100) PRIMARY KEY NOT NULL 77 | ); 78 | 79 | -- 80 | -- Table structure for table `oauth_users` 81 | -- 82 | 83 | CREATE TABLE `oauth_users` ( 84 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 85 | `username` varchar(320) UNIQUE NOT NULL, 86 | `password` varchar(100) NOT NULL, 87 | `first_name` varchar(80) DEFAULT NULL, 88 | `last_name` varchar(80) DEFAULT NULL 89 | ); 90 | 91 | CREATE INDEX `UNIQ_93804FF8F85E0677` ON oauth_users (`username`); 92 | -------------------------------------------------------------------------------- /data/oauth2_test.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO oauth_clients (name, secret, redirect, personal_access_client, password_client, is_confidential) 2 | VALUES ('client_test', '$2y$10$fFlZTo2Syqa./0JJ2QKV4O/Nfi9cqDMcwHBkN/WMcRLLlaxYUP2CK', '/redirect', 1, 1, 1), 3 | ('client_test2', '$2y$10$fFlZTo2Syqa./0JJ2QKV4O/Nfi9cqDMcwHBkN/WMcRLLlaxYUP2CK', '/redirect', 0, 0, 1), 4 | ('client_test_not_confidential', '$2y$10$fFlZTo2Syqa./0JJ2QKV4O/Nfi9cqDMcwHBkN/WMcRLLlaxYUP2CK', '/redirect', 0, 0, 0); 5 | 6 | INSERT INTO oauth_users (username, password) 7 | VALUES ('user_test', '$2y$10$DW12wQQvr4w7mQ.uSmz37OQkKcIZrRZnpXWoYue7b5v8E/pxvsAru'); 8 | 9 | INSERT INTO oauth_scopes (id) 10 | VALUES ('test'); 11 | -------------------------------------------------------------------------------- /src/AuthorizationHandler.php: -------------------------------------------------------------------------------- 1 | $responseFactory() 38 | ); 39 | } 40 | 41 | $this->responseFactory = $responseFactory; 42 | } 43 | 44 | public function handle(ServerRequestInterface $request): ResponseInterface 45 | { 46 | $authRequest = $request->getAttribute(AuthorizationRequest::class); 47 | return $this->server->completeAuthorizationRequest( 48 | $authRequest, 49 | $this->responseFactory->createResponse() 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/AuthorizationHandlerFactory.php: -------------------------------------------------------------------------------- 1 | get(AuthorizationServer::class), 18 | $this->detectResponseFactory($container) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AuthorizationMiddleware.php: -------------------------------------------------------------------------------- 1 | server = $server; 48 | if (is_callable($responseFactory)) { 49 | $responseFactory = new CallableResponseFactoryDecorator( 50 | static fn(): ResponseInterface => $responseFactory() 51 | ); 52 | } 53 | 54 | $this->responseFactory = $responseFactory; 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 61 | { 62 | try { 63 | $authRequest = $this->server->validateAuthorizationRequest($request); 64 | 65 | // The next handler must take care of providing the 66 | // authenticated user and the approval 67 | $authRequest->setAuthorizationApproved(false); 68 | 69 | return $handler->handle($request->withAttribute(AuthorizationRequest::class, $authRequest)); 70 | } catch (OAuthServerException $exception) { 71 | $response = $this->responseFactory->createResponse(); 72 | // The validation throws this exception if the request is not valid 73 | // for example when the client id is invalid 74 | return $exception->generateHttpResponse($response); 75 | } catch (BaseException $exception) { 76 | $response = $this->responseFactory->createResponse(); 77 | return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) 78 | ->generateHttpResponse($response); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AuthorizationMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | get(AuthorizationServer::class), 18 | $this->detectResponseFactory($container) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AuthorizationServerFactory.php: -------------------------------------------------------------------------------- 1 | getClientRepository($container); 32 | $accessTokenRepository = $this->getAccessTokenRepository($container); 33 | $scopeRepository = $this->getScopeRepository($container); 34 | 35 | $privateKey = $this->getCryptKey($this->getPrivateKey($container), 'authentication.private_key'); 36 | $encryptKey = $this->getEncryptionKey($container); 37 | $grants = $this->getGrantsConfig($container); 38 | 39 | $authServer = new AuthorizationServer( 40 | $clientRepository, 41 | $accessTokenRepository, 42 | $scopeRepository, 43 | $privateKey, 44 | $encryptKey 45 | ); 46 | 47 | $accessTokenInterval = new DateInterval($this->getAccessTokenExpire($container)); 48 | 49 | foreach ($grants as $grant) { 50 | // Config may set this grant to null. Continue on if grant has been disabled 51 | if (empty($grant)) { 52 | continue; 53 | } 54 | 55 | $authServer->enableGrantType( 56 | $container->get($grant), 57 | $accessTokenInterval 58 | ); 59 | } 60 | 61 | // add listeners if configured 62 | $this->addListeners($authServer, $container); 63 | 64 | // add listener providers if configured 65 | $this->addListenerProviders($authServer, $container); 66 | 67 | return $authServer; 68 | } 69 | 70 | /** 71 | * Optionally add event listeners 72 | */ 73 | private function addListeners( 74 | AuthorizationServer $authServer, 75 | ContainerInterface $container 76 | ): void { 77 | $listeners = $this->getListenersConfig($container); 78 | 79 | foreach ($listeners as $idx => $listenerConfig) { 80 | $event = $listenerConfig[0]; 81 | $listener = $listenerConfig[1]; 82 | $priority = $listenerConfig[2] ?? 0; 83 | if (is_string($listener)) { 84 | if (! $container->has($listener)) { 85 | throw new Exception\InvalidConfigException(sprintf( 86 | 'The second element of event_listeners config at ' 87 | . 'index "%s" is a string and therefore expected to ' 88 | . 'be available as a service key in the container. ' 89 | . 'A service named "%s" was not found.', 90 | $idx, 91 | $listener 92 | )); 93 | } 94 | $listener = $container->get($listener); 95 | } 96 | if (! is_int($priority)) { 97 | throw new Exception\InvalidConfigException(sprintf( 98 | 'The third element of event_listeners config at index "%s" (priority) ' 99 | . 'is expected to be an integer, received "%s"', 100 | $idx, 101 | $priority 102 | )); 103 | } 104 | $authServer->getEmitter() 105 | ->addListener($event, $listener, $priority); 106 | } 107 | } 108 | 109 | /** 110 | * Optionally add event listener providers 111 | */ 112 | private function addListenerProviders( 113 | AuthorizationServer $authServer, 114 | ContainerInterface $container 115 | ): void { 116 | $providers = $this->getListenerProvidersConfig($container); 117 | 118 | foreach ($providers as $idx => $provider) { 119 | if (is_string($provider)) { 120 | if (! $container->has($provider)) { 121 | throw new Exception\InvalidConfigException(sprintf( 122 | 'The event_listener_providers config at ' 123 | . 'index "%s" is a string and therefore expected to ' 124 | . 'be available as a service key in the container. ' 125 | . 'A service named "%s" was not found.', 126 | $idx, 127 | $provider 128 | )); 129 | } 130 | $provider = $container->get($provider); 131 | } 132 | $authServer->getEmitter()->useListenerProvider($provider); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 42 | 'authentication' => include __DIR__ . '/../config/oauth2.php', 43 | 'routes' => $this->getRoutes(), 44 | ]; 45 | } 46 | 47 | /** 48 | * Returns the container dependencies 49 | * 50 | * @return ServiceManagerConfiguration 51 | */ 52 | public function getDependencies(): array 53 | { 54 | return [ 55 | 'aliases' => [ 56 | // Choose a different adapter changing the alias value 57 | AccessTokenRepositoryInterface::class => Pdo\AccessTokenRepository::class, 58 | AuthCodeRepositoryInterface::class => Pdo\AuthCodeRepository::class, 59 | ClientRepositoryInterface::class => Pdo\ClientRepository::class, 60 | RefreshTokenRepositoryInterface::class => Pdo\RefreshTokenRepository::class, 61 | ScopeRepositoryInterface::class => Pdo\ScopeRepository::class, 62 | UserRepositoryInterface::class => Pdo\UserRepository::class, 63 | AuthenticationInterface::class => OAuth2Adapter::class, 64 | 65 | // Legacy Zend Framework aliases 66 | // @codingStandardsIgnoreStart 67 | 'Zend\Expressive\Authentication\AuthenticationInterface' => AuthenticationInterface::class, 68 | 'Zend\Expressive\Authentication\OAuth2\AuthorizationMiddleware' => AuthorizationMiddleware::class, 69 | 'Zend\Expressive\Authentication\OAuth2\AuthorizationHandler' => AuthorizationHandler::class, 70 | 'Zend\Expressive\Authentication\OAuth2\TokenEndpointHandler' => TokenEndpointHandler::class, 71 | 'Zend\Expressive\Authentication\OAuth2\OAuth2Adapter' => OAuth2Adapter::class, 72 | 'Zend\Expressive\Authentication\OAuth2\Repository\Pdo\PdoService' => Pdo\PdoService::class, 73 | 'Zend\Expressive\Authentication\OAuth2\Repository\Pdo\AccessTokenRepository' => Pdo\AccessTokenRepository::class, 74 | 'Zend\Expressive\Authentication\OAuth2\Repository\Pdo\AuthCodeRepository' => Pdo\AuthCodeRepository::class, 75 | 'Zend\Expressive\Authentication\OAuth2\Repository\Pdo\ClientRepository' => Pdo\ClientRepository::class, 76 | 'Zend\Expressive\Authentication\OAuth2\Repository\Pdo\RefreshTokenRepository' => Pdo\RefreshTokenRepository::class, 77 | 'Zend\Expressive\Authentication\OAuth2\Repository\Pdo\ScopeRepository' => Pdo\ScopeRepository::class, 78 | 'Zend\Expressive\Authentication\OAuth2\Repository\Pdo\UserRepository' => Pdo\UserRepository::class, 79 | 'Zend\Expressive\Authentication\OAuth2\PasswordGrant' => PasswordGrant::class, 80 | // @codingStandardsIgnoreEnd 81 | ], 82 | 'factories' => [ 83 | AuthorizationMiddleware::class => AuthorizationMiddlewareFactory::class, 84 | AuthorizationHandler::class => AuthorizationHandlerFactory::class, 85 | TokenEndpointHandler::class => TokenEndpointHandlerFactory::class, 86 | OAuth2Adapter::class => OAuth2AdapterFactory::class, 87 | AuthorizationServer::class => AuthorizationServerFactory::class, 88 | ResourceServer::class => ResourceServerFactory::class, 89 | // Pdo adapter 90 | Pdo\PdoService::class => Pdo\PdoServiceFactory::class, 91 | Pdo\AccessTokenRepository::class => Pdo\AccessTokenRepositoryFactory::class, 92 | Pdo\AuthCodeRepository::class => Pdo\AuthCodeRepositoryFactory::class, 93 | Pdo\ClientRepository::class => Pdo\ClientRepositoryFactory::class, 94 | Pdo\RefreshTokenRepository::class => Pdo\RefreshTokenRepositoryFactory::class, 95 | Pdo\ScopeRepository::class => Pdo\ScopeRepositoryFactory::class, 96 | Pdo\UserRepository::class => Pdo\UserRepositoryFactory::class, 97 | // Default Grants 98 | ClientCredentialsGrant::class => ClientCredentialsGrantFactory::class, 99 | PasswordGrant::class => PasswordGrantFactory::class, 100 | AuthCodeGrant::class => AuthCodeGrantFactory::class, 101 | ImplicitGrant::class => ImplicitGrantFactory::class, 102 | RefreshTokenGrant::class => RefreshTokenGrantFactory::class, 103 | ], 104 | ]; 105 | } 106 | 107 | public function getRoutes(): array 108 | { 109 | return [ 110 | [ 111 | 'name' => 'oauth', 112 | 'path' => '/oauth', 113 | 'middleware' => AuthorizationMiddleware::class, 114 | 'allowed_methods' => ['GET', 'POST'], 115 | ], 116 | ]; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ConfigTrait.php: -------------------------------------------------------------------------------- 1 | get('config')['authentication'] ?? []; 18 | 19 | if (! isset($config['private_key']) || empty($config['private_key'])) { 20 | throw new InvalidConfigException( 21 | 'The private_key value is missing in config authentication' 22 | ); 23 | } 24 | 25 | return $config['private_key']; 26 | } 27 | 28 | protected function getEncryptionKey(ContainerInterface $container): string 29 | { 30 | $config = $container->get('config')['authentication'] ?? []; 31 | 32 | if (! isset($config['encryption_key']) || empty($config['encryption_key'])) { 33 | throw new InvalidConfigException( 34 | 'The encryption_key value is missing in config authentication' 35 | ); 36 | } 37 | 38 | return $config['encryption_key']; 39 | } 40 | 41 | protected function getAccessTokenExpire(ContainerInterface $container): string 42 | { 43 | $config = $container->get('config')['authentication'] ?? []; 44 | 45 | if (! isset($config['access_token_expire'])) { 46 | throw new InvalidConfigException( 47 | 'The access_token_expire value is missing in config authentication' 48 | ); 49 | } 50 | 51 | return $config['access_token_expire']; 52 | } 53 | 54 | protected function getRefreshTokenExpire(ContainerInterface $container): string 55 | { 56 | $config = $container->get('config')['authentication'] ?? []; 57 | 58 | if (! isset($config['refresh_token_expire'])) { 59 | throw new InvalidConfigException( 60 | 'The refresh_token_expire value is missing in config authentication' 61 | ); 62 | } 63 | 64 | return $config['refresh_token_expire']; 65 | } 66 | 67 | protected function getAuthCodeExpire(ContainerInterface $container): string 68 | { 69 | $config = $container->get('config')['authentication'] ?? []; 70 | 71 | if (! isset($config['auth_code_expire'])) { 72 | throw new InvalidConfigException( 73 | 'The auth_code_expire value is missing in config authentication' 74 | ); 75 | } 76 | 77 | return $config['auth_code_expire']; 78 | } 79 | 80 | protected function getGrantsConfig(ContainerInterface $container): array 81 | { 82 | $config = $container->get('config')['authentication'] ?? []; 83 | 84 | if (empty($config['grants'])) { 85 | throw new InvalidConfigException( 86 | 'The grants value is missing in config authentication and must be an array' 87 | ); 88 | } 89 | if (! is_array($config['grants'])) { 90 | throw new InvalidConfigException( 91 | 'The grants must be an array value' 92 | ); 93 | } 94 | 95 | return $config['grants']; 96 | } 97 | 98 | protected function getListenersConfig(ContainerInterface $container): array 99 | { 100 | $config = $container->get('config')['authentication'] ?? []; 101 | 102 | if (empty($config['event_listeners'])) { 103 | return []; 104 | } 105 | if (! is_array($config['event_listeners'])) { 106 | throw new InvalidConfigException( 107 | 'The event_listeners config must be an array value' 108 | ); 109 | } 110 | 111 | return $config['event_listeners']; 112 | } 113 | 114 | protected function getListenerProvidersConfig(ContainerInterface $container): array 115 | { 116 | $config = $container->get('config')['authentication'] ?? []; 117 | 118 | if (empty($config['event_listener_providers'])) { 119 | return []; 120 | } 121 | if (! is_array($config['event_listener_providers'])) { 122 | throw new InvalidConfigException( 123 | 'The event_listener_providers config must be an array value' 124 | ); 125 | } 126 | 127 | return $config['event_listener_providers']; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/CryptKeyTrait.php: -------------------------------------------------------------------------------- 1 | setIdentifier($identifier); 37 | $this->name = $name; 38 | $this->redirectUri = explode(',', $redirectUri); 39 | $this->isConfidential = $isConfidential; 40 | } 41 | 42 | public function getSecret(): string 43 | { 44 | return $this->secret; 45 | } 46 | 47 | public function setSecret(string $secret): void 48 | { 49 | $this->secret = $secret; 50 | } 51 | 52 | public function hasPersonalAccessClient(): bool 53 | { 54 | return $this->personalAccessClient; 55 | } 56 | 57 | public function setPersonalAccessClient(bool $personalAccessClient): void 58 | { 59 | $this->personalAccessClient = $personalAccessClient; 60 | } 61 | 62 | public function hasPasswordClient(): bool 63 | { 64 | return $this->passwordClient; 65 | } 66 | 67 | public function setPasswordClient(bool $passwordClient): void 68 | { 69 | $this->passwordClient = $passwordClient; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Entity/RefreshTokenEntity.php: -------------------------------------------------------------------------------- 1 | revoked; 15 | } 16 | 17 | public function setRevoked(bool $revoked): void 18 | { 19 | $this->revoked = $revoked; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Entity/ScopeEntity.php: -------------------------------------------------------------------------------- 1 | getIdentifier(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Entity/TimestampableTrait.php: -------------------------------------------------------------------------------- 1 | createdAt; 24 | } 25 | 26 | public function setCreatedAt(DateTimeInterface $createdAt): void 27 | { 28 | $this->createdAt = $createdAt; 29 | } 30 | 31 | public function getUpdatedAt(): DateTimeInterface 32 | { 33 | return $this->updatedAt; 34 | } 35 | 36 | public function setUpdatedAt(DateTimeInterface $updatedAt): void 37 | { 38 | $this->updatedAt = $updatedAt; 39 | } 40 | 41 | /** 42 | * Set createdAt on current date/time if not set, using 43 | * timezone if defined 44 | */ 45 | public function timestampOnCreate(): void 46 | { 47 | if (! $this->createdAt) { 48 | if (method_exists($this, 'getTimezone')) { 49 | $this->createdAt = new DateTimeImmutable('now', new DateTimeZone($this->getTimezone()->getValue())); 50 | } else { 51 | $this->createdAt = new DateTimeImmutable(); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Entity/UserEntity.php: -------------------------------------------------------------------------------- 1 | setIdentifier($identifier); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getAuthCodeRepository($container), 22 | $this->getRefreshTokenRepository($container), 23 | new DateInterval($this->getAuthCodeExpire($container)) 24 | ); 25 | 26 | $grant->setRefreshTokenTTL( 27 | new DateInterval($this->getRefreshTokenExpire($container)) 28 | ); 29 | 30 | return $grant; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Grant/ClientCredentialsGrantFactory.php: -------------------------------------------------------------------------------- 1 | getAuthCodeExpire($container)) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Grant/PasswordGrantFactory.php: -------------------------------------------------------------------------------- 1 | getUserRepository($container), 22 | $this->getRefreshTokenRepository($container) 23 | ); 24 | 25 | $grant->setRefreshTokenTTL( 26 | new DateInterval($this->getRefreshTokenExpire($container)) 27 | ); 28 | 29 | return $grant; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Grant/RefreshTokenGrantFactory.php: -------------------------------------------------------------------------------- 1 | getRefreshTokenRepository($container) 23 | ); 24 | 25 | $grant->setRefreshTokenTTL( 26 | new DateInterval($this->getRefreshTokenExpire($container)) 27 | ); 28 | 29 | return $grant; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/OAuth2Adapter.php: -------------------------------------------------------------------------------- 1 | resourceServer = $resourceServer; 38 | 39 | if (is_callable($responseFactory)) { 40 | $responseFactory = new CallableResponseFactoryDecorator( 41 | static fn(): ResponseInterface => $responseFactory() 42 | ); 43 | } 44 | 45 | $this->responseFactory = $responseFactory; 46 | $this->userFactory = static fn(string $identity, array $roles = [], array $details = []): UserInterface 47 | => $userFactory($identity, $roles, $details); 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function authenticate(ServerRequestInterface $request): ?UserInterface 54 | { 55 | try { 56 | $result = $this->resourceServer->validateAuthenticatedRequest($request); 57 | $userId = $result->getAttribute('oauth_user_id', null); 58 | $clientId = $result->getAttribute('oauth_client_id', null); 59 | if (isset($userId)) { 60 | return ($this->userFactory)( 61 | $userId, 62 | [], 63 | [ 64 | 'oauth_user_id' => $userId, 65 | 'oauth_client_id' => $clientId, 66 | 'oauth_access_token_id' => $result->getAttribute('oauth_access_token_id', null), 67 | 'oauth_scopes' => $result->getAttribute('oauth_scopes', null), 68 | ] 69 | ); 70 | } 71 | } catch (OAuthServerException) { 72 | return null; 73 | } 74 | return null; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function unauthorizedResponse(ServerRequestInterface $request): ResponseInterface 81 | { 82 | return $this->responseFactory 83 | ->createResponse(401) 84 | ->withHeader( 85 | 'WWW-Authenticate', 86 | 'Bearer realm="OAuth2 token"' 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/OAuth2AdapterFactory.php: -------------------------------------------------------------------------------- 1 | has(ResourceServer::class) 18 | ? $container->get(ResourceServer::class) 19 | : null; 20 | 21 | if (null === $resourceServer) { 22 | throw new Exception\InvalidConfigException( 23 | 'OAuth2 resource server is missing for authentication' 24 | ); 25 | } 26 | 27 | return new OAuth2Adapter( 28 | $resourceServer, 29 | $this->detectResponseFactory($container), 30 | $container->get(UserInterface::class) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Psr17ResponseFactoryTrait.php: -------------------------------------------------------------------------------- 1 | has(ResponseFactoryInterface::class); 23 | 24 | if (! $psr17FactoryAvailable) { 25 | return $this->createResponseFactoryFromDeprecatedCallable($container); 26 | } 27 | 28 | if ($this->doesConfigurationProvidesDedicatedResponseFactory($container)) { 29 | return $this->createResponseFactoryFromDeprecatedCallable($container); 30 | } 31 | 32 | $responseFactory = $container->get(ResponseFactoryInterface::class); 33 | Assert::isInstanceOf($responseFactory, ResponseFactoryInterface::class); 34 | return $responseFactory; 35 | } 36 | 37 | private function createResponseFactoryFromDeprecatedCallable( 38 | ContainerInterface $container 39 | ): ResponseFactoryInterface { 40 | /** @var callable():ResponseInterface $responseFactory */ 41 | $responseFactory = $container->get(ResponseInterface::class); 42 | 43 | return new CallableResponseFactoryDecorator($responseFactory); 44 | } 45 | 46 | private function doesConfigurationProvidesDedicatedResponseFactory(ContainerInterface $container): bool 47 | { 48 | if (! $container->has('config')) { 49 | return false; 50 | } 51 | 52 | $config = $container->get('config'); 53 | Assert::isArrayAccessible($config); 54 | $dependencies = $config['dependencies'] ?? []; 55 | Assert::isMap($dependencies); 56 | 57 | $delegators = $dependencies['delegators'] ?? []; 58 | $aliases = $dependencies['aliases'] ?? []; 59 | Assert::isArrayAccessible($delegators); 60 | Assert::isArrayAccessible($aliases); 61 | 62 | if (isset($delegators[ResponseInterface::class]) || isset($aliases[ResponseInterface::class])) { 63 | // Even tho, aliases could point to a different service, we assume that there is a dedicated factory 64 | // available. The alias resolving is not worth it. 65 | return true; 66 | } 67 | 68 | /** @psalm-suppress MixedAssignment */ 69 | $deprecatedResponseFactory = $dependencies['factories'][ResponseInterface::class] ?? null; 70 | 71 | return $deprecatedResponseFactory !== null && $deprecatedResponseFactory !== ResponseFactoryFactory::class; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AbstractRepository.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 24 | } 25 | 26 | /** 27 | * Return a string of scopes, separated by space 28 | * from ScopeEntityInterface[] 29 | * 30 | * @param ScopeEntityInterface[] $scopes 31 | */ 32 | protected function scopesToString(array $scopes): string 33 | { 34 | if (empty($scopes)) { 35 | return ''; 36 | } 37 | 38 | return trim(array_reduce($scopes, static fn($result, $item): string => $result . ' ' . $item->getIdentifier())); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AccessTokenRepository.php: -------------------------------------------------------------------------------- 1 | setClient($clientEntity); 29 | foreach ($scopes as $scope) { 30 | $accessToken->addScope($scope); 31 | } 32 | $accessToken->setUserIdentifier($userIdentifier); 33 | return $accessToken; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity) 40 | { 41 | $columns = [ 42 | 'id', 43 | 'user_id', 44 | 'client_id', 45 | 'scopes', 46 | 'revoked', 47 | 'created_at', 48 | 'updated_at', 49 | 'expires_at', 50 | ]; 51 | 52 | $values = [ 53 | ':id', 54 | ':user_id', 55 | ':client_id', 56 | ':scopes', 57 | ':revoked', 58 | 'CURRENT_TIMESTAMP', 59 | 'CURRENT_TIMESTAMP', 60 | ':expires_at', 61 | ]; 62 | 63 | $sth = $this->pdo->prepare(sprintf( 64 | 'INSERT INTO oauth_access_tokens (%s) VALUES (%s)', 65 | implode(', ', $columns), 66 | implode(', ', $values) 67 | )); 68 | 69 | $params = [ 70 | ':id' => $accessTokenEntity->getIdentifier(), 71 | ':user_id' => $accessTokenEntity->getUserIdentifier(), 72 | ':client_id' => $accessTokenEntity->getClient()->getIdentifier(), 73 | ':scopes' => $this->scopesToString($accessTokenEntity->getScopes()), 74 | ':revoked' => 0, 75 | ':expires_at' => date( 76 | 'Y-m-d H:i:s', 77 | $accessTokenEntity->getExpiryDateTime()->getTimestamp() 78 | ), 79 | ]; 80 | 81 | if (false === $sth->execute($params)) { 82 | throw UniqueTokenIdentifierConstraintViolationException::create(); 83 | } 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public function revokeAccessToken($tokenId) 90 | { 91 | $sth = $this->pdo->prepare( 92 | 'UPDATE oauth_access_tokens SET revoked=:revoked WHERE id = :tokenId' 93 | ); 94 | $sth->bindValue(':revoked', 1); 95 | $sth->bindParam(':tokenId', $tokenId); 96 | 97 | $sth->execute(); 98 | } 99 | 100 | /** 101 | * {@inheritDoc} 102 | */ 103 | public function isAccessTokenRevoked($tokenId) 104 | { 105 | $sth = $this->pdo->prepare( 106 | 'SELECT revoked FROM oauth_access_tokens WHERE id = :tokenId' 107 | ); 108 | $sth->bindParam(':tokenId', $tokenId); 109 | 110 | if (false === $sth->execute()) { 111 | return false; 112 | } 113 | $row = $sth->fetch(); 114 | if (! is_array($row)) { 115 | throw OAuthServerException::invalidRefreshToken(); 116 | } 117 | 118 | return array_key_exists('revoked', $row) ? (bool) $row['revoked'] : false; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AccessTokenRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AuthCodeRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 30 | 'INSERT INTO oauth_auth_codes (id, user_id, client_id, scopes, revoked, expires_at) ' 31 | . 'VALUES (:id, :user_id, :client_id, :scopes, :revoked, :expires_at)' 32 | ); 33 | 34 | $sth->bindValue(':id', $authCodeEntity->getIdentifier()); 35 | $sth->bindValue(':user_id', $authCodeEntity->getUserIdentifier()); 36 | $sth->bindValue(':client_id', $authCodeEntity->getClient()->getIdentifier()); 37 | $sth->bindValue(':scopes', $this->scopesToString($authCodeEntity->getScopes())); 38 | $sth->bindValue(':revoked', 0); 39 | $sth->bindValue( 40 | ':expires_at', 41 | date( 42 | 'Y-m-d H:i:s', 43 | $authCodeEntity->getExpiryDateTime()->getTimestamp() 44 | ) 45 | ); 46 | 47 | if (false === $sth->execute()) { 48 | throw UniqueTokenIdentifierConstraintViolationException::create(); 49 | } 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public function revokeAuthCode($codeId) 56 | { 57 | $sth = $this->pdo->prepare( 58 | 'UPDATE oauth_auth_codes SET revoked=:revoked WHERE id = :codeId' 59 | ); 60 | $sth->bindValue(':revoked', 1); 61 | $sth->bindParam(':codeId', $codeId); 62 | 63 | $sth->execute(); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function isAuthCodeRevoked($codeId) 70 | { 71 | $sth = $this->pdo->prepare( 72 | 'SELECT revoked FROM oauth_auth_codes WHERE id = :codeId' 73 | ); 74 | $sth->bindParam(':codeId', $codeId); 75 | 76 | if (false === $sth->execute()) { 77 | return false; 78 | } 79 | $row = $sth->fetch(); 80 | 81 | return isset($row['revoked']) ? (bool) $row['revoked'] : false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AuthCodeRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ClientRepository.php: -------------------------------------------------------------------------------- 1 | getClientData($clientIdentifier); 21 | 22 | if ($clientData === null || $clientData === []) { 23 | return null; 24 | } 25 | 26 | return new ClientEntity( 27 | $clientIdentifier, 28 | $clientData['name'] ?? '', 29 | $clientData['redirect'] ?? '', 30 | (bool) ($clientData['is_confidential'] ?? null) 31 | ); 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function validateClient($clientIdentifier, $clientSecret, $grantType): bool 38 | { 39 | $clientData = $this->getClientData($clientIdentifier); 40 | 41 | if ($clientData === null || $clientData === []) { 42 | return false; 43 | } 44 | 45 | if (! $this->isGranted($clientData, $grantType)) { 46 | return false; 47 | } 48 | 49 | if (empty($clientData['secret']) || ! password_verify((string) $clientSecret, $clientData['secret'])) { 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | /** 57 | * Check the grantType for the client value, stored in $row 58 | */ 59 | protected function isGranted(array $row, ?string $grantType = null): bool 60 | { 61 | return match ($grantType) { 62 | 'authorization_code' => ! ($row['personal_access_client'] || $row['password_client']), 63 | 'personal_access' => (bool) $row['personal_access_client'], 64 | 'password' => (bool) $row['password_client'], 65 | default => true, 66 | }; 67 | } 68 | 69 | private function getClientData(string $clientIdentifier): ?array 70 | { 71 | $statement = $this->pdo->prepare( 72 | 'SELECT * FROM oauth_clients WHERE name = :clientIdentifier' 73 | ); 74 | $statement->bindParam(':clientIdentifier', $clientIdentifier); 75 | 76 | if ($statement->execute() === false) { 77 | return null; 78 | } 79 | 80 | $row = $statement->fetch(); 81 | 82 | if (empty($row)) { 83 | return null; 84 | } 85 | 86 | return $row; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ClientRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Repository/Pdo/PdoService.php: -------------------------------------------------------------------------------- 1 | has('config') ? $container->get('config') : []; 18 | $config = $config['authentication']['pdo'] ?? null; 19 | if (null === $config) { 20 | throw new Exception\InvalidConfigException( 21 | 'The PDO configuration is missing' 22 | ); 23 | } 24 | 25 | if (is_string($config) && ! $container->has($config)) { 26 | throw new Exception\InvalidConfigException( 27 | 'Invalid service for PDO' 28 | ); 29 | } 30 | 31 | if (is_string($config) && $container->has($config)) { 32 | return $container->get($config); 33 | } 34 | 35 | if (! isset($config['dsn'])) { 36 | throw new Exception\InvalidConfigException( 37 | 'The DSN configuration is missing for PDO' 38 | ); 39 | } 40 | $username = $config['username'] ?? null; 41 | $password = $config['password'] ?? null; 42 | return new PdoService($config['dsn'], $username, $password); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Repository/Pdo/RefreshTokenRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 24 | 'INSERT INTO oauth_refresh_tokens (id, access_token_id, revoked, expires_at) ' 25 | . 'VALUES (:id, :access_token_id, :revoked, :expires_at)' 26 | ); 27 | 28 | $sth->bindValue(':id', $refreshTokenEntity->getIdentifier()); 29 | $sth->bindValue(':access_token_id', $refreshTokenEntity->getAccessToken()->getIdentifier()); 30 | $sth->bindValue(':revoked', 0); 31 | $sth->bindValue( 32 | ':expires_at', 33 | date( 34 | 'Y-m-d H:i:s', 35 | $refreshTokenEntity->getExpiryDateTime()->getTimestamp() 36 | ) 37 | ); 38 | 39 | if (false === $sth->execute()) { 40 | throw UniqueTokenIdentifierConstraintViolationException::create(); 41 | } 42 | } 43 | 44 | /** 45 | * @param string $tokenId 46 | */ 47 | public function revokeRefreshToken($tokenId) 48 | { 49 | $sth = $this->pdo->prepare( 50 | 'UPDATE oauth_refresh_tokens SET revoked=:revoked WHERE id = :tokenId' 51 | ); 52 | $sth->bindValue(':revoked', 1); 53 | $sth->bindParam(':tokenId', $tokenId); 54 | 55 | $sth->execute(); 56 | } 57 | 58 | /** 59 | * @param string $tokenId 60 | */ 61 | public function isRefreshTokenRevoked($tokenId): bool 62 | { 63 | $sth = $this->pdo->prepare( 64 | 'SELECT revoked FROM oauth_refresh_tokens WHERE id = :tokenId' 65 | ); 66 | $sth->bindParam(':tokenId', $tokenId); 67 | 68 | if (false === $sth->execute()) { 69 | return false; 70 | } 71 | $row = $sth->fetch(); 72 | 73 | return isset($row['revoked']) ? (bool) $row['revoked'] : false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Repository/Pdo/RefreshTokenRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ScopeRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 21 | 'SELECT id FROM oauth_scopes WHERE id = :identifier' 22 | ); 23 | $sth->bindParam(':identifier', $identifier); 24 | 25 | if (false === $sth->execute()) { 26 | return; 27 | } 28 | 29 | $row = $sth->fetch(); 30 | if (! isset($row['id'])) { 31 | return; 32 | } 33 | 34 | $scope = new ScopeEntity(); 35 | $scope->setIdentifier($row['id']); 36 | return $scope; 37 | } 38 | 39 | /** 40 | * @param ScopeEntityInterface[] $scopes 41 | * @param string $grantType 42 | * @param null|string $userIdentifier 43 | * @return ScopeEntityInterface[] 44 | */ 45 | public function finalizeScopes( 46 | array $scopes, 47 | $grantType, 48 | ClientEntityInterface $clientEntity, 49 | $userIdentifier = null 50 | ): array { 51 | return $scopes; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ScopeRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Repository/Pdo/UserRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 28 | 'SELECT password FROM oauth_users WHERE username = :username' 29 | ); 30 | $sth->bindParam(':username', $username); 31 | 32 | if (false === $sth->execute()) { 33 | return; 34 | } 35 | 36 | $row = $sth->fetch(); 37 | 38 | if (! empty($row) && password_verify($password, $row['password'])) { 39 | return new UserEntity($username); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Repository/Pdo/UserRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RepositoryTrait.php: -------------------------------------------------------------------------------- 1 | has(UserRepositoryInterface::class)) { 20 | throw new Exception\InvalidConfigException( 21 | 'OAuth2 User Repository is missing' 22 | ); 23 | } 24 | return $container->get(UserRepositoryInterface::class); 25 | } 26 | 27 | protected function getScopeRepository(ContainerInterface $container): ScopeRepositoryInterface 28 | { 29 | if (! $container->has(ScopeRepositoryInterface::class)) { 30 | throw new Exception\InvalidConfigException( 31 | 'OAuth2 Scope Repository is missing' 32 | ); 33 | } 34 | return $container->get(ScopeRepositoryInterface::class); 35 | } 36 | 37 | protected function getAccessTokenRepository(ContainerInterface $container): AccessTokenRepositoryInterface 38 | { 39 | if (! $container->has(AccessTokenRepositoryInterface::class)) { 40 | throw new Exception\InvalidConfigException( 41 | 'OAuth2 Access Token Repository is missing' 42 | ); 43 | } 44 | return $container->get(AccessTokenRepositoryInterface::class); 45 | } 46 | 47 | protected function getClientRepository(ContainerInterface $container): ClientRepositoryInterface 48 | { 49 | if (! $container->has(ClientRepositoryInterface::class)) { 50 | throw new Exception\InvalidConfigException( 51 | 'OAuth2 Client Repository is missing' 52 | ); 53 | } 54 | return $container->get(ClientRepositoryInterface::class); 55 | } 56 | 57 | protected function getRefreshTokenRepository(ContainerInterface $container): RefreshTokenRepositoryInterface 58 | { 59 | if (! $container->has(RefreshTokenRepositoryInterface::class)) { 60 | throw new Exception\InvalidConfigException( 61 | 'OAuth2 Refresk Token Repository is missing' 62 | ); 63 | } 64 | return $container->get(RefreshTokenRepositoryInterface::class); 65 | } 66 | 67 | protected function getAuthCodeRepository(ContainerInterface $container): AuthCodeRepositoryInterface 68 | { 69 | if (! $container->has(AuthCodeRepositoryInterface::class)) { 70 | throw new Exception\InvalidConfigException( 71 | 'OAuth2 Refresk Token Repository is missing' 72 | ); 73 | } 74 | return $container->get(AuthCodeRepositoryInterface::class); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ResourceServerFactory.php: -------------------------------------------------------------------------------- 1 | has('config') ? $container->get('config') : []; 18 | $config = $config['authentication'] ?? []; 19 | 20 | if (! isset($config['public_key'])) { 21 | throw new Exception\InvalidConfigException( 22 | 'The public_key value is missing in config authentication' 23 | ); 24 | } 25 | 26 | $publicKey = $this->getCryptKey($config['public_key'], 'authentication.public_key'); 27 | 28 | return new ResourceServer( 29 | $this->getAccessTokenRepository($container), 30 | $publicKey 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Response/CallableResponseFactoryDecorator.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 25 | } 26 | 27 | public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface 28 | { 29 | return $this->getResponseFromCallable()->withStatus($code, $reasonPhrase); 30 | } 31 | 32 | public function getResponseFromCallable(): ResponseInterface 33 | { 34 | return ($this->responseFactory)(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/TokenEndpointHandler.php: -------------------------------------------------------------------------------- 1 | server = $server; 41 | if (is_callable($responseFactory)) { 42 | $responseFactory = new CallableResponseFactoryDecorator( 43 | static fn(): ResponseInterface => $responseFactory() 44 | ); 45 | } 46 | $this->responseFactory = $responseFactory; 47 | } 48 | 49 | private function createResponse(): ResponseInterface 50 | { 51 | return $this->responseFactory->createResponse(); 52 | } 53 | 54 | /** 55 | * Request an access token 56 | * 57 | * Used for client credential grant, password grant, and refresh token grant 58 | * 59 | * @see https://oauth2.thephpleague.com/authorization-server/client-credentials-grant/ 60 | * @see https://oauth2.thephpleague.com/authorization-server/resource-owner-password-credentials-grant/ 61 | * @see https://oauth2.thephpleague.com/authorization-server/refresh-token-grant/ 62 | * @see https://tools.ietf.org/html/rfc6749#section-3.2 63 | */ 64 | public function handle(ServerRequestInterface $request): ResponseInterface 65 | { 66 | $response = $this->createResponse(); 67 | 68 | try { 69 | return $this->server->respondToAccessTokenRequest($request, $response); 70 | } catch (OAuthServerException $exception) { 71 | return $exception->generateHttpResponse($response); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/TokenEndpointHandlerFactory.php: -------------------------------------------------------------------------------- 1 | get(AuthorizationServer::class), 18 | $this->detectResponseFactory($container) 19 | ); 20 | } 21 | } 22 | --------------------------------------------------------------------------------