├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Command ├── ClearExpiredTokensCommand.php ├── ClearRevokedTokensCommand.php ├── CreateClientCommand.php ├── DeleteClientCommand.php ├── ListClientsCommand.php └── UpdateClientCommand.php ├── Controller ├── AuthorizationController.php └── TokenController.php ├── Converter ├── ScopeConverter.php ├── ScopeConverterInterface.php ├── UserConverter.php └── UserConverterInterface.php ├── DBAL └── Type │ ├── Grant.php │ ├── ImplodedArray.php │ ├── RedirectUri.php │ └── Scope.php ├── DependencyInjection ├── Configuration.php ├── Security │ └── OAuth2Factory.php └── TrikoderOAuth2Extension.php ├── Event ├── AbstractUserResolveEvent.php ├── AuthorizationRequestResolveEvent.php ├── AuthorizationRequestResolveEventFactory.php ├── ScopeResolveEvent.php └── UserResolveEvent.php ├── EventListener ├── AuthorizationRequestUserResolvingListener.php └── ConvertExceptionToResponseListener.php ├── LICENSE.md ├── League ├── AuthorizationServer │ ├── GrantConfigurator.php │ └── GrantTypeInterface.php ├── Entity │ ├── AccessToken.php │ ├── AuthCode.php │ ├── Client.php │ ├── RefreshToken.php │ ├── Scope.php │ └── User.php └── Repository │ ├── AccessTokenRepository.php │ ├── AuthCodeRepository.php │ ├── ClientRepository.php │ ├── RefreshTokenRepository.php │ ├── ScopeRepository.php │ └── UserRepository.php ├── Manager ├── AccessTokenManagerInterface.php ├── AuthorizationCodeManagerInterface.php ├── ClientFilter.php ├── ClientManagerInterface.php ├── Doctrine │ ├── AccessTokenManager.php │ ├── AuthorizationCodeManager.php │ ├── ClientManager.php │ └── RefreshTokenManager.php ├── InMemory │ ├── AccessTokenManager.php │ ├── AuthorizationCodeManager.php │ ├── ClientManager.php │ ├── RefreshTokenManager.php │ └── ScopeManager.php ├── RefreshTokenManagerInterface.php └── ScopeManagerInterface.php ├── Model ├── AccessToken.php ├── AuthorizationCode.php ├── Client.php ├── Grant.php ├── RedirectUri.php ├── RefreshToken.php └── Scope.php ├── OAuth2Events.php ├── OAuth2Grants.php ├── README.md ├── Resources └── config │ ├── doctrine │ └── model │ │ ├── AccessToken.orm.xml │ │ ├── AuthorizationCode.orm.xml │ │ ├── Client.orm.xml │ │ └── RefreshToken.orm.xml │ ├── routes.xml │ ├── services.xml │ └── storage │ ├── doctrine.xml │ └── in_memory.xml ├── Security ├── Authentication │ ├── Provider │ │ └── OAuth2Provider.php │ └── Token │ │ ├── OAuth2Token.php │ │ └── OAuth2TokenFactory.php ├── EntryPoint │ └── OAuth2EntryPoint.php ├── Exception │ ├── InsufficientScopesException.php │ └── Oauth2AuthenticationFailedException.php ├── Firewall │ └── OAuth2Listener.php ├── Guard │ └── Authenticator │ │ └── OAuth2Authenticator.php └── User │ └── NullUser.php ├── Service ├── ClientFinderInterface.php ├── CredentialsRevoker │ └── DoctrineCredentialsRevoker.php └── CredentialsRevokerInterface.php ├── TrikoderOAuth2Bundle.php ├── UPGRADE.md ├── composer.json └── docs ├── basic-setup.md ├── controlling-token-scopes.md ├── debugging.md ├── implementing-custom-grant-type.md ├── password-grant-handling.md ├── psr-implementation-switching.md └── resources └── phpstorm-xdebug.png /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 9 | ->append([__FILE__]) 10 | ; 11 | 12 | return (new PhpCsFixer\Config()) 13 | ->setUsingCache(true) 14 | ->setRules([ 15 | '@DoctrineAnnotation' => true, 16 | '@PSR2' => true, 17 | '@Symfony' => true, 18 | '@Symfony:risky' => true, 19 | 'array_indentation' => true, 20 | 'class_definition' => ['multi_line_extends_each_single_line' => true], 21 | 'compact_nullable_typehint' => true, 22 | 'concat_space' => ['spacing' => 'one'], 23 | 'declare_strict_types' => true, 24 | 'heredoc_to_nowdoc' => true, 25 | 'list_syntax' => ['syntax' => 'short'], 26 | 'no_null_property_initialization' => true, 27 | 'no_superfluous_phpdoc_tags' => true, 28 | 'nullable_type_declaration_for_default_null_value' => true, 29 | 'no_trailing_whitespace_in_string' => false, 30 | 'ordered_imports' => [ 31 | 'imports_order' => [ 32 | OrderedImportsFixer::IMPORT_TYPE_CONST, 33 | OrderedImportsFixer::IMPORT_TYPE_FUNCTION, 34 | OrderedImportsFixer::IMPORT_TYPE_CLASS, 35 | ], 36 | ], 37 | 'pow_to_exponentiation' => true, 38 | 'phpdoc_align' => ['align' => 'left'], 39 | 'phpdoc_summary' => false, 40 | 'ternary_to_null_coalescing' => true, 41 | ]) 42 | ->setRiskyAllowed(true) 43 | ->setFinder($finder) 44 | ; 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project [adheres to Semantic Versioning, but only for the public API](README.md#versioning). 6 | 7 | ## [3.2.0] - 2020-10-26 8 | 9 | ### Added 10 | - Jobs with the `prefer-lowest` composer flag to CI ([#204](https://github.com/trikoder/oauth2-bundle/pull/204)) 11 | - On delete `CASCADE` on authorization code entity client association ([#216](https://github.com/trikoder/oauth2-bundle/pull/216)) 12 | - `Trikoder\Bundle\OAuth2Bundle\Event\AbstractUserResolveEvent` abstract class for user resolve events ([#221](https://github.com/trikoder/oauth2-bundle/pull/221)) 13 | - Add per grant type configuration options ([#199](https://github.com/trikoder/oauth2-bundle/pull/199)) 14 | - CI testing - Symfony 5.1 ([#230](https://github.com/trikoder/oauth2-bundle/pull/230)) 15 | - Cleanup command (`trikoder:oauth2:clear-revoked-tokens`) for revoked tokens ([#234](https://github.com/trikoder/oauth2-bundle/pull/234)) 16 | - Setter for the `secret` property of the `Client` Doctrine entity ([#239](https://github.com/trikoder/oauth2-bundle/pull/239)) 17 | 18 | ### Changed 19 | - Pass previous exception to`Oauth2AuthenticationFailedException` exception ([#223](https://github.com/trikoder/oauth2-bundle/pull/223)) 20 | - Allow PHPUnit 9 ([#238](https://github.com/trikoder/oauth2-bundle/pull/238)) 21 | 22 | ### Deprecated 23 | - Legacy service aliases ([#203](https://github.com/trikoder/oauth2-bundle/pull/203)) 24 | 25 | ## [3.1.1] - 2020-04-10 26 | ### Removed 27 | - `userIdentifier` index from `oauth2_access_token` and `oauth2_authorization_code` tables ([6108915](https://github.com/trikoder/oauth2-bundle/commit/6108915ee83c5597160d2be9669966b20d0e461f)) 28 | 29 | ## [3.1.0] - 2020-04-09 30 | ### Added 31 | - Ability to revoke credentials (access tokens, authorization codes and refresh tokens) programmatically ([fee109d](https://github.com/trikoder/oauth2-bundle/commit/fee109da2d52d73dfc81501d0af3d66216f09de6)) 32 | - Support for registering custom grant types ([6b37588](https://github.com/trikoder/oauth2-bundle/commit/6b3758807b7cca6835c00504f1632ea78de563a5)) 33 | 34 | ### Fixed 35 | - Console command `trikoder:oauth2:list-clients` not being able to list clients without a secret ([da38b7a](https://github.com/trikoder/oauth2-bundle/commit/da38b7ab77060b4d43aca405559d5ffbd7a34d8d)) 36 | 37 | ## [3.0.0] - 2020-02-26 38 | ### Added 39 | - Ability to restrict clients from using the `plain` challenge method during PKCE ([4562a1f](https://github.com/trikoder/oauth2-bundle/commit/4562a1ff306375fd651aa91c85d0d4fd6f4c1b13)) 40 | - Ability to clear expired authorization codes ([91b6447](https://github.com/trikoder/oauth2-bundle/commit/91b6447257419d8e961c4f5b0abd187f1b735856)) 41 | - Support for defining public (non-confidential) clients ([8a71f55](https://github.com/trikoder/oauth2-bundle/commit/8a71f55aa1482d00cee66684141cc9ef81d31f31)) 42 | - The bundle is now compatible with Symfony 5.x ([3f36977](https://github.com/trikoder/oauth2-bundle/commit/3f369771385c0b90855da712b9cb31faa4c651dc)) 43 | 44 | ### Changed 45 | - [PSR-7 Bridge](https://github.com/symfony/psr-http-message-bridge) version constraint to `^2.0` ([3c741ca](https://github.com/trikoder/oauth2-bundle/commit/3c741ca1e394886e8936ad018c28cd1ddd3dff02)) 46 | - The bundle now relies on `8.x` versions of [league/oauth2-server](https://github.com/thephpleague/oauth2-server) for base functionality ([8becc18](https://github.com/trikoder/oauth2-bundle/commit/8becc18255052a73d0f76a030be9de0fe9868928)) 47 | 48 | ### Removed 49 | - Support for Symfony 3.4, 4.2 and 4.3 ([3f36977](https://github.com/trikoder/oauth2-bundle/commit/3f369771385c0b90855da712b9cb31faa4c651dc)) 50 | 51 | ## [2.1.1] - 2020-02-25 52 | ### Added 53 | - The bundle is now additionally tested against PHP 7.4 ([2b29be3](https://github.com/trikoder/oauth2-bundle/commit/2b29be3629877a648f4a199b96185b40d625f6aa)) 54 | 55 | ### Fixed 56 | - Authentication provider not being aware of the current firewall context ([d349329](https://github.com/trikoder/oauth2-bundle/commit/d349329056c219969e097ae6bd3eb724968f9812)) 57 | - Faulty logic when revoking authorization codes ([24ad882](https://github.com/trikoder/oauth2-bundle/commit/24ad88211cefddf97170f5c1cc8ba1e5cf285e42)) 58 | 59 | ## [2.1.0] - 2019-12-09 60 | ### Added 61 | - Ability to change the scope role prefix using the `role_prefix` configuration option ([b2ee617](https://github.com/trikoder/oauth2-bundle/commit/b2ee6179832cc142d95e3b13d9af09d6cb6831d5)) 62 | - Interfaces for converter type service classes ([d2caf69](https://github.com/trikoder/oauth2-bundle/commit/d2caf690839523a2c84d967a6f99787898d4c654)) 63 | - New testing target in Travis CI for Symfony 4.4 ([8a44fd4](https://github.com/trikoder/oauth2-bundle/commit/8a44fd4d7673467cc4f69988424cdfc677767aab)) 64 | - The bundle is now fully compatible with [Symfony Flex](https://github.com/symfony/flex) ([a4ccea1](https://github.com/trikoder/oauth2-bundle/commit/a4ccea1dfaaba6d95daf3e1f1a84952cafb65d01)) 65 | 66 | ### Changed 67 | - [DoctrineBundle](https://github.com/doctrine/DoctrineBundle) version constraint to allow `2.x` derived versions ([885e398](https://github.com/trikoder/oauth2-bundle/commit/885e39811331e89bae99bca71f1a783497d26d12)) 68 | - Explicitly list [league/oauth2-server](https://github.com/thephpleague/oauth2-server) version requirements in the documentation ([9dce66a](https://github.com/trikoder/oauth2-bundle/commit/9dce66a089c33c224fe5cb58bdfd6285350a607b)) 69 | - Reduce distributed package size by excluding files that are used only for development ([80b9e41](https://github.com/trikoder/oauth2-bundle/commit/80b9e41155e7a94c3b1a4602c8daa25cc6d246b2)) 70 | - Simplify `AuthorizationRequestResolveEvent` class creation ([32908c1](https://github.com/trikoder/oauth2-bundle/commit/32908c1a4a89fd89d5835d4de931d237de223b50)) 71 | 72 | ### Fixed 73 | - Not being able to delete clients that have access/refresh tokens assigned to them ([424b770](https://github.com/trikoder/oauth2-bundle/commit/424b770dbd99e4651777a3fa26186a756b4e93c4)) 74 | 75 | ## [2.0.1] - 2019-08-13 76 | ### Removed 77 | - PSR-7/17 alias check during the container compile process ([0847ea3](https://github.com/trikoder/oauth2-bundle/commit/0847ea3034cc433c9c8f92ec46fedbdace259e3d)) 78 | 79 | ## [2.0.0] - 2019-08-08 80 | ### Added 81 | - Ability to specify a [Defuse](https://github.com/defuse/php-encryption/blob/master/docs/classes/Key.md) key as the encryption key ([d83fefe](https://github.com/trikoder/oauth2-bundle/commit/d83fefe149c1add841d4225ebc2a32aa9333308d)) 82 | - Ability to use different PSR-7/17 HTTP transport implementations ([4973e1c](https://github.com/trikoder/oauth2-bundle/commit/4973e1c7ddfc4afcca85989bde1b8d28dcd7fd4a)) 83 | - Allow configuration of the private key passphrase ([f16ec67](https://github.com/trikoder/oauth2-bundle/commit/f16ec67f2fa8dbf8fedd78488d625cef2db5b90d)) 84 | - Checks if dependent bundles are enabled in the application kernel ([38f6641](https://github.com/trikoder/oauth2-bundle/commit/38f66418b5f28b8666d5bbde1e36a45cfc166afa)) 85 | - Console command for clearing expired access and refresh tokens ([de3e338](https://github.com/trikoder/oauth2-bundle/commit/de3e338a24e0b03ab634c4982c46034715635379)) 86 | - Console commands for client management ([2425b3d](https://github.com/trikoder/oauth2-bundle/commit/2425b3d149cadb1706eb70b321491bf894114784), [56aafba](https://github.com/trikoder/oauth2-bundle/commit/56aafba995f06e45fd6521735be780c327e67d65)) 87 | - Server grant types can now be enabled/disabled through bundle configuration ([baffa92](https://github.com/trikoder/oauth2-bundle/commit/baffa928d9f489bd642fff7ae2bc88ce93badcbf)) 88 | - Support for the "authorization_code" server grant type ([a61114a](https://github.com/trikoder/oauth2-bundle/commit/a61114a7f2449bdb28b0779b0a4a7d21b9fff2c2)) 89 | - Support for the "implicit" server grant type ([91b3d75](https://github.com/trikoder/oauth2-bundle/commit/91b3d7583e269d5151927f24fbaec9d2fc4cea3d)) 90 | - Support for Symfony 4.3 ([e4cf668](https://github.com/trikoder/oauth2-bundle/commit/e4cf6680ddfb7d1327b2c83ed22f46c0db56c67a)) 91 | - The bundle is now additionally tested against PHP 7.3 ([9f5937b](https://github.com/trikoder/oauth2-bundle/commit/9f5937bda2a112337a9b375ed3923918bcc06370)) 92 | 93 | ### Changed 94 | - Authentication exceptions are now thrown instead of setting the response object ([8a505f6](https://github.com/trikoder/oauth2-bundle/commit/8a505f61f52d6ce924ab7119a411a17efdf1bbef)) 95 | - Modernize bundle service definitions ([fc1f855](https://github.com/trikoder/oauth2-bundle/commit/fc1f8556c180ba961bd6f2c973d36ff7439cbf34), [ef2f557](https://github.com/trikoder/oauth2-bundle/commit/ef2f557f357de8cf39bd87da3499cb38563ad82f)) 96 | - Previously [documented](https://github.com/trikoder/oauth2-bundle/blob/v1.1.0/docs/controlling-token-scopes.md) client scope inheriting and restricting is now the new default behavior ([af9bffc](https://github.com/trikoder/oauth2-bundle/commit/af9bffcbcab7b02036c36ba0e1bc7d7b6921280)) 97 | - Relaxed the [league/oauth2-server](https://github.com/thephpleague/oauth2-server) package version constraint to allow non-braking changes ([26d9c0b](https://github.com/trikoder/oauth2-bundle/commit/26d9c0b14a4d31e3fd5f620facfa374795f9adeb)) 98 | - Use `DateTimeInterface` instead of `DateTime` whenever possible ([4549252](https://github.com/trikoder/oauth2-bundle/commit/454925249bfba1b6fd5c8e07fd64a4e87039759e)) 99 | 100 | ### Fixed 101 | - [DoctrineBundle](https://github.com/doctrine/DoctrineBundle) related deprecation notices ([fbde15b](https://github.com/trikoder/oauth2-bundle/commit/fbde15bfd2295b10563136701f668c839dcc1e5e)) 102 | - Not being able to override the "persistence" config tree from other configuration files ([b62b331](https://github.com/trikoder/oauth2-bundle/commit/b62b331834c77609893a1b70633ef7683ada7edc)) 103 | - [Symfony](https://github.com/symfony/symfony) related deprecation notices ([601d482](https://github.com/trikoder/oauth2-bundle/commit/601d482351e67d3d22b6ca600e26ed1da7f33866)) 104 | 105 | ### Removed 106 | - Redundant configuration node options ([5fa60ef](https://github.com/trikoder/oauth2-bundle/commit/5fa60efb81fddea79989e502f67bc7aca1bcac16)) 107 | - Support for Symfony 4.1 ([4973e1c](https://github.com/trikoder/oauth2-bundle/commit/4973e1c7ddfc4afcca85989bde1b8d28dcd7fd4a)) 108 | - Unsupported HTTP verbs on the `/authorize` and `/token` endpoints ([51ef5ae](https://github.com/trikoder/oauth2-bundle/commit/51ef5ae7e659afaf63c024e7da070464d318fd67)) 109 | 110 | ## [1.1.0] - 2019-01-07 111 | ### Added 112 | - The bundle is now compatible with Symfony 3.4 ([0ba9cb3](https://github.com/trikoder/oauth2-bundle/commit/0ba9cb306157a9ad89691eb3d20054a6803af472)) 113 | 114 | ### Changed 115 | - Bundle dependency requirements are now more relaxed ([158d221](https://github.com/trikoder/oauth2-bundle/commit/158d2212ff7d8aab802bcd87def6917522d1fbce)) 116 | - Permission checks against private/public keys are no longer enforced ([a24415a](https://github.com/trikoder/oauth2-bundle/commit/a24415a560174783a51ecfcd86a644490389cb13)) 117 | 118 | ### Fixed 119 | - Bundle creating a `default` Doctrine connection if it didn't exist ([d4e58a0](https://github.com/trikoder/oauth2-bundle/commit/d4e58a04eff3cc442fa6f9d721984b4c5ceedf67)) 120 | - Improper class naming ([b43be3d](https://github.com/trikoder/oauth2-bundle/commit/b43be3d9ac9bc3d5daa43daac61e4939326a13bd)) 121 | 122 | ## [1.0.0] - 2018-11-28 123 | This is the initial release. 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are **welcome** and **very much appreciated**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/trikoder/oauth2-bundle). 6 | 7 | ## Pull Request guidelines 8 | 9 | - **Add tests!** - We strongly encourage adding tests as well since the PR might not be accepted without them. 10 | 11 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 12 | 13 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 14 | 15 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 16 | 17 | ## Development 18 | 19 | [Docker](https://www.docker.com/) 18.03+ and [Docker Compose](https://github.com/docker/compose) 1.13+ are required for the development environment. 20 | 21 | ### Building the environment 22 | 23 | Make sure your Docker images are all built and up-to-date using the following command: 24 | 25 | ```sh 26 | dev/bin/docker-compose build 27 | ``` 28 | 29 | > **NOTE:** You can target a different version of PHP during development by appending the `--build-arg PHP_VERSION=` argument. 30 | 31 | After that, install all the needed packages required to develop the project: 32 | 33 | ```sh 34 | dev/bin/php composer install 35 | ``` 36 | 37 | ### Debugging 38 | 39 | You can run the debugger using the following command: 40 | 41 | ```sh 42 | dev/bin/php-debug vendor/bin/phpunit 43 | ``` 44 | 45 | Make sure your IDE is setup properly, for more information check out the [dedicated documentation](docs/debugging.md). 46 | 47 | ### Code linting 48 | 49 | This bundle enforces the PSR-2 and Symfony code standards during development by using the [PHP CS Fixer](https://cs.sensiolabs.org/) utility. Before committing any code, you can run the utility to fix any potential rule violations: 50 | 51 | ```sh 52 | dev/bin/php composer lint 53 | ``` 54 | 55 | ### Testing 56 | 57 | You can run the whole test suite using the following command: 58 | 59 | ```sh 60 | dev/bin/php-test composer test 61 | ``` 62 | 63 | **Happy coding**! 64 | -------------------------------------------------------------------------------- /Command/ClearExpiredTokensCommand.php: -------------------------------------------------------------------------------- 1 | accessTokenManager = $accessTokenManager; 43 | $this->refreshTokenManager = $refreshTokenManager; 44 | $this->authorizationCodeManager = $authorizationCodeManager; 45 | } 46 | 47 | protected function configure(): void 48 | { 49 | $this 50 | ->setDescription('Clears all expired access and/or refresh tokens and/or auth codes') 51 | ->addOption( 52 | 'access-tokens', 53 | 'a', 54 | InputOption::VALUE_NONE, 55 | 'Clear expired access tokens.' 56 | ) 57 | ->addOption( 58 | 'refresh-tokens', 59 | 'r', 60 | InputOption::VALUE_NONE, 61 | 'Clear expired refresh tokens.' 62 | ) 63 | ->addOption( 64 | 'auth-codes', 65 | 'c', 66 | InputOption::VALUE_NONE, 67 | 'Clear expired auth codes.' 68 | ) 69 | ; 70 | } 71 | 72 | protected function execute(InputInterface $input, OutputInterface $output): int 73 | { 74 | $io = new SymfonyStyle($input, $output); 75 | 76 | $clearExpiredAccessTokens = $input->getOption('access-tokens'); 77 | $clearExpiredRefreshTokens = $input->getOption('refresh-tokens'); 78 | $clearExpiredAuthCodes = $input->getOption('auth-codes'); 79 | 80 | if (!$clearExpiredAccessTokens && !$clearExpiredRefreshTokens && !$clearExpiredAuthCodes) { 81 | $this->clearExpiredAccessTokens($io); 82 | $this->clearExpiredRefreshTokens($io); 83 | $this->clearExpiredAuthCodes($io); 84 | 85 | return 0; 86 | } 87 | 88 | if (true === $clearExpiredAccessTokens) { 89 | $this->clearExpiredAccessTokens($io); 90 | } 91 | 92 | if (true === $clearExpiredRefreshTokens) { 93 | $this->clearExpiredRefreshTokens($io); 94 | } 95 | 96 | if (true === $clearExpiredAuthCodes) { 97 | $this->clearExpiredAuthCodes($io); 98 | } 99 | 100 | return 0; 101 | } 102 | 103 | private function clearExpiredAccessTokens(SymfonyStyle $io): void 104 | { 105 | $numOfClearedAccessTokens = $this->accessTokenManager->clearExpired(); 106 | $io->success(sprintf( 107 | 'Cleared %d expired access token%s.', 108 | $numOfClearedAccessTokens, 109 | 1 === $numOfClearedAccessTokens ? '' : 's' 110 | )); 111 | } 112 | 113 | private function clearExpiredRefreshTokens(SymfonyStyle $io): void 114 | { 115 | $numOfClearedRefreshTokens = $this->refreshTokenManager->clearExpired(); 116 | $io->success(sprintf( 117 | 'Cleared %d expired refresh token%s.', 118 | $numOfClearedRefreshTokens, 119 | 1 === $numOfClearedRefreshTokens ? '' : 's' 120 | )); 121 | } 122 | 123 | private function clearExpiredAuthCodes(SymfonyStyle $io): void 124 | { 125 | $numOfClearedAuthCodes = $this->authorizationCodeManager->clearExpired(); 126 | $io->success(sprintf( 127 | 'Cleared %d expired auth code%s.', 128 | $numOfClearedAuthCodes, 129 | 1 === $numOfClearedAuthCodes ? '' : 's' 130 | )); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Command/ClearRevokedTokensCommand.php: -------------------------------------------------------------------------------- 1 | accessTokenManager = $accessTokenManager; 42 | $this->refreshTokenManager = $refreshTokenManager; 43 | $this->authorizationCodeManager = $authorizationCodeManager; 44 | } 45 | 46 | protected function configure(): void 47 | { 48 | $this 49 | ->setDescription('Clears all revoked access and/or refresh tokens and/or auth codes') 50 | ->addOption( 51 | 'access-tokens', 52 | 'a', 53 | InputOption::VALUE_NONE, 54 | 'Clear revoked access tokens.' 55 | ) 56 | ->addOption( 57 | 'refresh-tokens', 58 | 'r', 59 | InputOption::VALUE_NONE, 60 | 'Clear revoked refresh tokens.' 61 | ) 62 | ->addOption( 63 | 'auth-codes', 64 | 'c', 65 | InputOption::VALUE_NONE, 66 | 'Clear revoked auth codes.' 67 | ) 68 | ; 69 | } 70 | 71 | protected function execute(InputInterface $input, OutputInterface $output): int 72 | { 73 | $clearRevokedAccessTokens = $input->getOption('access-tokens'); 74 | $clearRevokedRefreshTokens = $input->getOption('refresh-tokens'); 75 | $clearRevokedAuthCodes = $input->getOption('auth-codes'); 76 | 77 | if (!$clearRevokedAccessTokens && !$clearRevokedRefreshTokens && !$clearRevokedAuthCodes) { 78 | $clearRevokedAccessTokens = true; 79 | $clearRevokedRefreshTokens = true; 80 | $clearRevokedAuthCodes = true; 81 | } 82 | 83 | if (true === $clearRevokedAccessTokens && $this->clearRevokedMethodExists($output, $this->accessTokenManager)) { 84 | $affected = $this->accessTokenManager->clearRevoked(); 85 | $output->writeln( 86 | sprintf( 87 | 'Access tokens deleted: %s.', 88 | $affected 89 | ) 90 | ); 91 | } 92 | 93 | if (true === $clearRevokedRefreshTokens && $this->clearRevokedMethodExists($output, $this->refreshTokenManager)) { 94 | $affected = $this->refreshTokenManager->clearRevoked(); 95 | $output->writeln( 96 | sprintf( 97 | 'Refresh tokens deleted: %s.', 98 | $affected 99 | ) 100 | ); 101 | } 102 | 103 | if (true === $clearRevokedAuthCodes && $this->clearRevokedMethodExists($output, $this->authorizationCodeManager)) { 104 | $affected = $this->authorizationCodeManager->clearRevoked(); 105 | $output->writeln( 106 | sprintf( 107 | 'Auth codes deleted: %s.', 108 | $affected 109 | ) 110 | ); 111 | } 112 | 113 | return 0; 114 | } 115 | 116 | private function clearRevokedMethodExists(OutputInterface $output, object $manager): bool 117 | { 118 | $methodName = 'clearRevoked'; 119 | $exists = method_exists($manager, $methodName); 120 | 121 | if (!$exists) { 122 | $output->writeln( 123 | sprintf( 124 | 'Method "%s:%s()" will be required in the next major release. Skipping for now...', 125 | \get_class($manager), 126 | $methodName 127 | ) 128 | ); 129 | } 130 | 131 | return $exists; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Command/CreateClientCommand.php: -------------------------------------------------------------------------------- 1 | clientManager = $clientManager; 34 | } 35 | 36 | protected function configure(): void 37 | { 38 | $this 39 | ->setDescription('Creates a new oAuth2 client') 40 | ->addOption( 41 | 'redirect-uri', 42 | null, 43 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 44 | 'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.', 45 | [] 46 | ) 47 | ->addOption( 48 | 'grant-type', 49 | null, 50 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 51 | 'Sets allowed grant type for client. Use this option multiple times to set multiple grant types.', 52 | [] 53 | ) 54 | ->addOption( 55 | 'scope', 56 | null, 57 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 58 | 'Sets allowed scope for client. Use this option multiple times to set multiple scopes.', 59 | [] 60 | ) 61 | ->addArgument( 62 | 'identifier', 63 | InputArgument::OPTIONAL, 64 | 'The client identifier' 65 | ) 66 | ->addArgument( 67 | 'secret', 68 | InputArgument::OPTIONAL, 69 | 'The client secret' 70 | ) 71 | ->addOption( 72 | 'public', 73 | null, 74 | InputOption::VALUE_NONE, 75 | 'Create a public client.' 76 | ) 77 | ->addOption( 78 | 'allow-plain-text-pkce', 79 | null, 80 | InputOption::VALUE_NONE, 81 | 'Create a client who is allowed to use plain challenge method for PKCE.' 82 | ) 83 | ; 84 | } 85 | 86 | protected function execute(InputInterface $input, OutputInterface $output): int 87 | { 88 | $io = new SymfonyStyle($input, $output); 89 | 90 | try { 91 | $client = $this->buildClientFromInput($input); 92 | } catch (InvalidArgumentException $exception) { 93 | $io->error($exception->getMessage()); 94 | 95 | return 1; 96 | } 97 | 98 | $this->clientManager->save($client); 99 | $io->success('New oAuth2 client created successfully.'); 100 | 101 | $headers = ['Identifier', 'Secret']; 102 | $rows = [ 103 | [$client->getIdentifier(), $client->getSecret()], 104 | ]; 105 | $io->table($headers, $rows); 106 | 107 | return 0; 108 | } 109 | 110 | private function buildClientFromInput(InputInterface $input): Client 111 | { 112 | $identifier = $input->getArgument('identifier') ?? hash('md5', random_bytes(16)); 113 | 114 | $isPublic = $input->getOption('public'); 115 | 116 | if (null !== $input->getArgument('secret') && $isPublic) { 117 | throw new InvalidArgumentException('The client cannot have a secret and be public.'); 118 | } 119 | 120 | $secret = $isPublic ? null : $input->getArgument('secret') ?? hash('sha512', random_bytes(32)); 121 | 122 | $client = new Client($identifier, $secret); 123 | $client->setActive(true); 124 | $client->setAllowPlainTextPkce($input->getOption('allow-plain-text-pkce')); 125 | 126 | $redirectUris = array_map( 127 | static function (string $redirectUri): RedirectUri { return new RedirectUri($redirectUri); }, 128 | $input->getOption('redirect-uri') 129 | ); 130 | $client->setRedirectUris(...$redirectUris); 131 | 132 | $grants = array_map( 133 | static function (string $grant): Grant { return new Grant($grant); }, 134 | $input->getOption('grant-type') 135 | ); 136 | $client->setGrants(...$grants); 137 | 138 | $scopes = array_map( 139 | static function (string $scope): Scope { return new Scope($scope); }, 140 | $input->getOption('scope') 141 | ); 142 | $client->setScopes(...$scopes); 143 | 144 | return $client; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Command/DeleteClientCommand.php: -------------------------------------------------------------------------------- 1 | clientManager = $clientManager; 28 | } 29 | 30 | protected function configure(): void 31 | { 32 | $this 33 | ->setDescription('Deletes an oAuth2 client') 34 | ->addArgument( 35 | 'identifier', 36 | InputArgument::REQUIRED, 37 | 'The client ID' 38 | ) 39 | ; 40 | } 41 | 42 | protected function execute(InputInterface $input, OutputInterface $output): int 43 | { 44 | $io = new SymfonyStyle($input, $output); 45 | $identifier = $input->getArgument('identifier'); 46 | $client = $this->clientManager->find($identifier); 47 | if (null === $client) { 48 | $io->error(sprintf('oAuth2 client identified as "%s" does not exist', $identifier)); 49 | 50 | return 1; 51 | } 52 | $this->clientManager->remove($client); 53 | $io->success('Given oAuth2 client deleted successfully.'); 54 | 55 | return 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Command/ListClientsCommand.php: -------------------------------------------------------------------------------- 1 | clientManager = $clientManager; 35 | } 36 | 37 | protected function configure(): void 38 | { 39 | $this 40 | ->setDescription('Lists existing oAuth2 clients') 41 | ->addOption( 42 | 'columns', 43 | null, 44 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 45 | 'Determine which columns are shown. Can be used multiple times to specify multiple columns.', 46 | self::ALLOWED_COLUMNS 47 | ) 48 | ->addOption( 49 | 'redirect-uri', 50 | null, 51 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 52 | 'Finds by redirect uri for client. Use this option multiple times to filter by multiple redirect URIs.', 53 | [] 54 | ) 55 | ->addOption( 56 | 'grant-type', 57 | null, 58 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 59 | 'Finds by allowed grant type for client. Use this option multiple times to filter by multiple grant types.', 60 | [] 61 | ) 62 | ->addOption( 63 | 'scope', 64 | null, 65 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 66 | 'Finds by allowed scope for client. Use this option multiple times to find by multiple scopes.', 67 | [] 68 | ) 69 | ; 70 | } 71 | 72 | protected function execute(InputInterface $input, OutputInterface $output): int 73 | { 74 | $criteria = $this->getFindByCriteria($input); 75 | $clients = $this->clientManager->list($criteria); 76 | $this->drawTable($input, $output, $clients); 77 | 78 | return 0; 79 | } 80 | 81 | private function getFindByCriteria(InputInterface $input): ClientFilter 82 | { 83 | return 84 | ClientFilter 85 | ::create() 86 | ->addGrantCriteria(...array_map(static function (string $grant): Grant { 87 | return new Grant($grant); 88 | }, $input->getOption('grant-type'))) 89 | ->addRedirectUriCriteria(...array_map(static function (string $redirectUri): RedirectUri { 90 | return new RedirectUri($redirectUri); 91 | }, $input->getOption('redirect-uri'))) 92 | ->addScopeCriteria(...array_map(static function (string $scope): Scope { 93 | return new Scope($scope); 94 | }, $input->getOption('scope'))) 95 | ; 96 | } 97 | 98 | private function drawTable(InputInterface $input, OutputInterface $output, array $clients): void 99 | { 100 | $io = new SymfonyStyle($input, $output); 101 | $columns = $this->getColumns($input); 102 | $rows = $this->getRows($clients, $columns); 103 | $io->table($columns, $rows); 104 | } 105 | 106 | private function getRows(array $clients, array $columns): array 107 | { 108 | return array_map(static function (Client $client) use ($columns): array { 109 | $values = [ 110 | 'identifier' => $client->getIdentifier(), 111 | 'secret' => $client->getSecret(), 112 | 'scope' => implode(', ', $client->getScopes()), 113 | 'redirect uri' => implode(', ', $client->getRedirectUris()), 114 | 'grant type' => implode(', ', $client->getGrants()), 115 | ]; 116 | 117 | return array_map(static function (string $column) use ($values): string { 118 | return $values[$column] ?? ''; 119 | }, $columns); 120 | }, $clients); 121 | } 122 | 123 | private function getColumns(InputInterface $input): array 124 | { 125 | $requestedColumns = $input->getOption('columns'); 126 | $requestedColumns = array_map(static function (string $column): string { 127 | return strtolower(trim($column)); 128 | }, $requestedColumns); 129 | 130 | return array_intersect($requestedColumns, self::ALLOWED_COLUMNS); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Command/UpdateClientCommand.php: -------------------------------------------------------------------------------- 1 | clientManager = $clientManager; 33 | } 34 | 35 | protected function configure(): void 36 | { 37 | $this 38 | ->setDescription('Updates an oAuth2 client') 39 | ->addOption( 40 | 'redirect-uri', 41 | null, 42 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 43 | 'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.', 44 | [] 45 | ) 46 | ->addOption( 47 | 'grant-type', 48 | null, 49 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 50 | 'Sets allowed grant type for client. Use this option multiple times to set multiple grant types.', 51 | [] 52 | ) 53 | ->addOption( 54 | 'scope', 55 | null, 56 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 57 | 'Sets allowed scope for client. Use this option multiple times to set multiple scopes.', 58 | [] 59 | ) 60 | ->addOption( 61 | 'deactivated', 62 | null, 63 | InputOption::VALUE_NONE, 64 | 'If provided, it will deactivate the given client.' 65 | ) 66 | ->addArgument( 67 | 'identifier', 68 | InputArgument::REQUIRED, 69 | 'The client ID' 70 | ) 71 | ; 72 | } 73 | 74 | protected function execute(InputInterface $input, OutputInterface $output): int 75 | { 76 | $io = new SymfonyStyle($input, $output); 77 | 78 | if (null === $client = $this->clientManager->find($input->getArgument('identifier'))) { 79 | $io->error(sprintf('oAuth2 client identified as "%s"', $input->getArgument('identifier'))); 80 | 81 | return 1; 82 | } 83 | 84 | $client = $this->updateClientFromInput($client, $input); 85 | $this->clientManager->save($client); 86 | $io->success('Given oAuth2 client updated successfully.'); 87 | 88 | return 0; 89 | } 90 | 91 | private function updateClientFromInput(Client $client, InputInterface $input): Client 92 | { 93 | $client->setActive(!$input->getOption('deactivated')); 94 | 95 | $redirectUris = array_map( 96 | static function (string $redirectUri): RedirectUri { return new RedirectUri($redirectUri); }, 97 | $input->getOption('redirect-uri') 98 | ); 99 | $client->setRedirectUris(...$redirectUris); 100 | 101 | $grants = array_map( 102 | static function (string $grant): Grant { return new Grant($grant); }, 103 | $input->getOption('grant-type') 104 | ); 105 | $client->setGrants(...$grants); 106 | 107 | $scopes = array_map( 108 | static function (string $scope): Scope { return new Scope($scope); }, 109 | $input->getOption('scope') 110 | ); 111 | $client->setScopes(...$scopes); 112 | 113 | return $client; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Controller/AuthorizationController.php: -------------------------------------------------------------------------------- 1 | server = $server; 54 | $this->eventDispatcher = $eventDispatcher; 55 | $this->eventFactory = $eventFactory; 56 | $this->userConverter = $userConverter; 57 | $this->clientManager = $clientManager; 58 | } 59 | 60 | public function indexAction(ServerRequestInterface $serverRequest, ResponseFactoryInterface $responseFactory): ResponseInterface 61 | { 62 | $serverResponse = $responseFactory->createResponse(); 63 | 64 | try { 65 | $authRequest = $this->server->validateAuthorizationRequest($serverRequest); 66 | 67 | if ('plain' === $authRequest->getCodeChallengeMethod()) { 68 | $client = $this->clientManager->find($authRequest->getClient()->getIdentifier()); 69 | if (!$client->isPlainTextPkceAllowed()) { 70 | return OAuthServerException::invalidRequest( 71 | 'code_challenge_method', 72 | 'Plain code challenge method is not allowed for this client' 73 | )->generateHttpResponse($serverResponse); 74 | } 75 | } 76 | 77 | /** @var AuthorizationRequestResolveEvent $event */ 78 | $event = $this->eventDispatcher->dispatch( 79 | $this->eventFactory->fromAuthorizationRequest($authRequest), 80 | OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE 81 | ); 82 | 83 | $authRequest->setUser($this->userConverter->toLeague($event->getUser())); 84 | 85 | if ($event->hasResponse()) { 86 | return $event->getResponse(); 87 | } 88 | 89 | $authRequest->setAuthorizationApproved($event->getAuthorizationResolution()); 90 | 91 | return $this->server->completeAuthorizationRequest($authRequest, $serverResponse); 92 | } catch (OAuthServerException $e) { 93 | return $e->generateHttpResponse($serverResponse); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Controller/TokenController.php: -------------------------------------------------------------------------------- 1 | server = $server; 23 | } 24 | 25 | public function indexAction( 26 | ServerRequestInterface $serverRequest, 27 | ResponseFactoryInterface $responseFactory 28 | ): ResponseInterface { 29 | $serverResponse = $responseFactory->createResponse(); 30 | 31 | try { 32 | return $this->server->respondToAccessTokenRequest($serverRequest, $serverResponse); 33 | } catch (OAuthServerException $e) { 34 | return $e->generateHttpResponse($serverResponse); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Converter/ScopeConverter.php: -------------------------------------------------------------------------------- 1 | getIdentifier()); 15 | } 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function toDomainArray(array $scopes): array 21 | { 22 | return array_map(function (ScopeEntity $scope): ScopeModel { 23 | return $this->toDomain($scope); 24 | }, $scopes); 25 | } 26 | 27 | public function toLeague(ScopeModel $scope): ScopeEntity 28 | { 29 | $scopeEntity = new ScopeEntity(); 30 | $scopeEntity->setIdentifier((string) $scope); 31 | 32 | return $scopeEntity; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function toLeagueArray(array $scopes): array 39 | { 40 | return array_map(function (ScopeModel $scope): ScopeEntity { 41 | return $this->toLeague($scope); 42 | }, $scopes); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Converter/ScopeConverterInterface.php: -------------------------------------------------------------------------------- 1 | setIdentifier($user->getUsername()); 18 | } 19 | 20 | return $userEntity; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Converter/UserConverterInterface.php: -------------------------------------------------------------------------------- 1 | assertValueCanBeImploded($item); 34 | } 35 | 36 | return implode(self::VALUE_DELIMITER, $value); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function convertToPHPValue($value, AbstractPlatform $platform) 43 | { 44 | if (null === $value) { 45 | return []; 46 | } 47 | 48 | $values = explode(self::VALUE_DELIMITER, $value); 49 | 50 | return $this->convertDatabaseValues($values); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) 57 | { 58 | $fieldDeclaration['length'] = 65535; 59 | 60 | return parent::getSQLDeclaration($fieldDeclaration, $platform); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function requiresSQLCommentHint(AbstractPlatform $platform) 67 | { 68 | return true; 69 | } 70 | 71 | private function assertValueCanBeImploded($value): void 72 | { 73 | if (null === $value) { 74 | return; 75 | } 76 | 77 | if (is_scalar($value)) { 78 | return; 79 | } 80 | 81 | if (\is_object($value) && method_exists($value, '__toString')) { 82 | return; 83 | } 84 | 85 | throw new InvalidArgumentException(sprintf('The value of \'%s\' type cannot be imploded.', \gettype($value))); 86 | } 87 | 88 | abstract protected function convertDatabaseValues(array $values): array; 89 | } 90 | -------------------------------------------------------------------------------- /DBAL/Type/RedirectUri.php: -------------------------------------------------------------------------------- 1 | setDefinition($providerId, new ChildDefinition(OAuth2Provider::class)) 26 | ->replaceArgument('$userProvider', new Reference($userProvider)) 27 | ->replaceArgument('$providerKey', $id); 28 | 29 | $listenerId = 'security.authentication.listener.oauth2.' . $id; 30 | $container 31 | ->setDefinition($listenerId, new ChildDefinition(OAuth2Listener::class)) 32 | ->replaceArgument('$providerKey', $id); 33 | 34 | return [$providerId, $listenerId, OAuth2EntryPoint::class]; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function getPosition() 41 | { 42 | return 'pre_auth'; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function getKey() 49 | { 50 | return 'oauth2'; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function addConfiguration(NodeDefinition $node) 57 | { 58 | return; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /DependencyInjection/TrikoderOAuth2Extension.php: -------------------------------------------------------------------------------- 1 | load('services.xml'); 58 | 59 | $config = $this->processConfiguration(new Configuration(), $configs); 60 | 61 | $this->configurePersistence($loader, $container, $config['persistence']); 62 | $this->configureAuthorizationServer($container, $config['authorization_server']); 63 | $this->configureResourceServer($container, $config['resource_server']); 64 | $this->configureScopes($container, $config['scopes']); 65 | 66 | $container->getDefinition(OAuth2TokenFactory::class) 67 | ->setArgument(0, $config['role_prefix']); 68 | 69 | $container->getDefinition(ConvertExceptionToResponseListener::class) 70 | ->addTag('kernel.event_listener', [ 71 | 'event' => KernelEvents::EXCEPTION, 72 | 'method' => 'onKernelException', 73 | 'priority' => $config['exception_event_listener_priority'], 74 | ]); 75 | 76 | $container->registerForAutoconfiguration(GrantTypeInterface::class) 77 | ->addTag('trikoder.oauth2.authorization_server.grant'); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getAlias() 84 | { 85 | return 'trikoder_oauth2'; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function prepend(ContainerBuilder $container) 92 | { 93 | $container->prependExtensionConfig('doctrine', [ 94 | 'dbal' => [ 95 | 'connections' => null, 96 | 'types' => [ 97 | 'oauth2_grant' => GrantType::class, 98 | 'oauth2_redirect_uri' => RedirectUriType::class, 99 | 'oauth2_scope' => ScopeType::class, 100 | ], 101 | ], 102 | ]); 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function process(ContainerBuilder $container) 109 | { 110 | $this->assertRequiredBundlesAreEnabled($container); 111 | } 112 | 113 | private function assertRequiredBundlesAreEnabled(ContainerBuilder $container): void 114 | { 115 | $requiredBundles = [ 116 | 'doctrine' => DoctrineBundle::class, 117 | 'security' => SecurityBundle::class, 118 | 'ajgarlag_psr_http_message' => AjgarlagPsrHttpMessageBundle::class, 119 | ]; 120 | 121 | foreach ($requiredBundles as $bundleAlias => $requiredBundle) { 122 | if (!$container->hasExtension($bundleAlias)) { 123 | throw new LogicException(sprintf('Bundle \'%s\' needs to be enabled in your application kernel.', $requiredBundle)); 124 | } 125 | } 126 | } 127 | 128 | private function configureAuthorizationServer(ContainerBuilder $container, array $config): void 129 | { 130 | $authorizationServer = $container 131 | ->getDefinition(AuthorizationServer::class) 132 | ->replaceArgument('$privateKey', new Definition(CryptKey::class, [ 133 | $config['private_key'], 134 | $config['private_key_passphrase'], 135 | false, 136 | ])); 137 | 138 | if ('plain' === $config['encryption_key_type']) { 139 | $authorizationServer->replaceArgument('$encryptionKey', $config['encryption_key']); 140 | } elseif ('defuse' === $config['encryption_key_type']) { 141 | if (!class_exists(Key::class)) { 142 | throw new RuntimeException('You must install the "defuse/php-encryption" package to use "encryption_key_type: defuse".'); 143 | } 144 | 145 | $keyDefinition = (new Definition(Key::class)) 146 | ->setFactory([Key::class, 'loadFromAsciiSafeString']) 147 | ->addArgument($config['encryption_key']); 148 | $container->setDefinition('trikoder.oauth2.defuse_key', $keyDefinition); 149 | 150 | $authorizationServer->replaceArgument('$encryptionKey', new Reference('trikoder.oauth2.defuse_key')); 151 | } 152 | 153 | $grantTypes = $config['grant_types']; 154 | 155 | if ($grantTypes['client_credentials']['enable']) { 156 | $authorizationServer->addMethodCall('enableGrantType', [ 157 | new Reference(ClientCredentialsGrant::class), 158 | new Definition(DateInterval::class, [$grantTypes['client_credentials']['access_token_ttl']]), 159 | ]); 160 | } 161 | 162 | if ($grantTypes['password']['enable']) { 163 | $authorizationServer->addMethodCall('enableGrantType', [ 164 | new Reference(PasswordGrant::class), 165 | new Definition(DateInterval::class, [$grantTypes['password']['access_token_ttl']]), 166 | ]); 167 | } 168 | 169 | if ($grantTypes['refresh_token']['enable']) { 170 | $authorizationServer->addMethodCall('enableGrantType', [ 171 | new Reference(RefreshTokenGrant::class), 172 | new Definition(DateInterval::class, [$grantTypes['refresh_token']['access_token_ttl']]), 173 | ]); 174 | } 175 | 176 | if ($grantTypes['authorization_code']['enable']) { 177 | $authorizationServer->addMethodCall('enableGrantType', [ 178 | new Reference(AuthCodeGrant::class), 179 | new Definition(DateInterval::class, [$grantTypes['authorization_code']['access_token_ttl']]), 180 | ]); 181 | } 182 | 183 | if ($grantTypes['implicit']['enable']) { 184 | $authorizationServer->addMethodCall('enableGrantType', [ 185 | new Reference(ImplicitGrant::class), 186 | new Definition(DateInterval::class, [$grantTypes['implicit']['access_token_ttl']]), 187 | ]); 188 | } 189 | 190 | $this->configureGrants($container, $config); 191 | } 192 | 193 | private function configureGrants(ContainerBuilder $container, array $config): void 194 | { 195 | $grantTypes = $config['grant_types']; 196 | 197 | $container 198 | ->getDefinition(PasswordGrant::class) 199 | ->addMethodCall('setRefreshTokenTTL', [ 200 | new Definition(DateInterval::class, [$grantTypes['password']['refresh_token_ttl']]), 201 | ]) 202 | ; 203 | 204 | $container 205 | ->getDefinition(RefreshTokenGrant::class) 206 | ->addMethodCall('setRefreshTokenTTL', [ 207 | new Definition(DateInterval::class, [$grantTypes['refresh_token']['refresh_token_ttl']]), 208 | ]) 209 | ; 210 | 211 | $authCodeGrantDefinition = $container->getDefinition(AuthCodeGrant::class); 212 | $authCodeGrantDefinition 213 | ->replaceArgument('$authCodeTTL', new Definition(DateInterval::class, [$grantTypes['authorization_code']['auth_code_ttl']])) 214 | ->addMethodCall('setRefreshTokenTTL', [ 215 | new Definition(DateInterval::class, [$grantTypes['authorization_code']['refresh_token_ttl']]), 216 | ]) 217 | ; 218 | 219 | if (false === $grantTypes['authorization_code']['require_code_challenge_for_public_clients']) { 220 | $authCodeGrantDefinition->addMethodCall('disableRequireCodeChallengeForPublicClients'); 221 | } 222 | 223 | $container 224 | ->getDefinition(ImplicitGrant::class) 225 | ->replaceArgument('$accessTokenTTL', new Definition(DateInterval::class, [$grantTypes['implicit']['access_token_ttl']])) 226 | ; 227 | } 228 | 229 | /** 230 | * @throws Exception 231 | */ 232 | private function configurePersistence(LoaderInterface $loader, ContainerBuilder $container, array $config): void 233 | { 234 | if (\count($config) > 1) { 235 | throw new LogicException('Only one persistence method can be configured at a time.'); 236 | } 237 | 238 | $persistenceConfiguration = current($config); 239 | $persistenceMethod = key($config); 240 | 241 | switch ($persistenceMethod) { 242 | case 'in_memory': 243 | $loader->load('storage/in_memory.xml'); 244 | $this->configureInMemoryPersistence($container); 245 | break; 246 | case 'doctrine': 247 | $loader->load('storage/doctrine.xml'); 248 | $this->configureDoctrinePersistence($container, $persistenceConfiguration); 249 | break; 250 | } 251 | } 252 | 253 | private function configureDoctrinePersistence(ContainerBuilder $container, array $config): void 254 | { 255 | $entityManagerName = $config['entity_manager']; 256 | 257 | $entityManager = new Reference( 258 | sprintf('doctrine.orm.%s_entity_manager', $entityManagerName) 259 | ); 260 | 261 | $container 262 | ->getDefinition(AccessTokenManager::class) 263 | ->replaceArgument('$entityManager', $entityManager) 264 | ; 265 | 266 | $container 267 | ->getDefinition(ClientManager::class) 268 | ->replaceArgument('$entityManager', $entityManager) 269 | ; 270 | 271 | $container 272 | ->getDefinition(RefreshTokenManager::class) 273 | ->replaceArgument('$entityManager', $entityManager) 274 | ; 275 | 276 | $container 277 | ->getDefinition(AuthorizationCodeManager::class) 278 | ->replaceArgument('$entityManager', $entityManager) 279 | ; 280 | 281 | $container 282 | ->getDefinition(DoctrineCredentialsRevoker::class) 283 | ->replaceArgument('$entityManager', $entityManager) 284 | ; 285 | 286 | $container->setParameter('trikoder.oauth2.persistence.doctrine.enabled', true); 287 | $container->setParameter('trikoder.oauth2.persistence.doctrine.manager', $entityManagerName); 288 | } 289 | 290 | private function configureInMemoryPersistence(ContainerBuilder $container): void 291 | { 292 | $container->setParameter('trikoder.oauth2.persistence.in_memory.enabled', true); 293 | } 294 | 295 | private function configureResourceServer(ContainerBuilder $container, array $config): void 296 | { 297 | $container 298 | ->getDefinition(ResourceServer::class) 299 | ->replaceArgument('$publicKey', new Definition(CryptKey::class, [ 300 | $config['public_key'], 301 | null, 302 | false, 303 | ])) 304 | ; 305 | } 306 | 307 | private function configureScopes(ContainerBuilder $container, array $scopes): void 308 | { 309 | $scopeManager = $container 310 | ->getDefinition( 311 | (string) $container->getAlias(ScopeManagerInterface::class) 312 | ) 313 | ; 314 | 315 | foreach ($scopes as $scope) { 316 | $scopeManager->addMethodCall('save', [ 317 | new Definition(ScopeModel::class, [$scope]), 318 | ]); 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /Event/AbstractUserResolveEvent.php: -------------------------------------------------------------------------------- 1 | user; 23 | } 24 | 25 | public function setUser(?UserInterface $user): self 26 | { 27 | $this->user = $user; 28 | 29 | return $this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Event/AuthorizationRequestResolveEvent.php: -------------------------------------------------------------------------------- 1 | authorizationRequest = $authorizationRequest; 56 | $this->scopes = $scopes; 57 | $this->client = $client; 58 | } 59 | 60 | public function getAuthorizationResolution(): bool 61 | { 62 | return $this->authorizationResolution; 63 | } 64 | 65 | public function resolveAuthorization(bool $authorizationResolution): self 66 | { 67 | $this->authorizationResolution = $authorizationResolution; 68 | $this->response = null; 69 | $this->stopPropagation(); 70 | 71 | return $this; 72 | } 73 | 74 | public function hasResponse(): bool 75 | { 76 | return $this->response instanceof ResponseInterface; 77 | } 78 | 79 | public function getResponse(): ResponseInterface 80 | { 81 | if (!$this->hasResponse()) { 82 | throw new LogicException('There is no response. You should call "hasResponse" to check if the response exists.'); 83 | } 84 | 85 | return $this->response; 86 | } 87 | 88 | public function setResponse(ResponseInterface $response): self 89 | { 90 | $this->response = $response; 91 | $this->stopPropagation(); 92 | 93 | return $this; 94 | } 95 | 96 | public function getGrantTypeId(): string 97 | { 98 | return $this->authorizationRequest->getGrantTypeId(); 99 | } 100 | 101 | public function getClient(): Client 102 | { 103 | return $this->client; 104 | } 105 | 106 | public function getUser(): ?UserInterface 107 | { 108 | return $this->user; 109 | } 110 | 111 | public function setUser(?UserInterface $user): self 112 | { 113 | $this->user = $user; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * @return Scope[] 120 | */ 121 | public function getScopes(): array 122 | { 123 | return $this->scopes; 124 | } 125 | 126 | public function isAuthorizationApproved(): bool 127 | { 128 | return $this->authorizationRequest->isAuthorizationApproved(); 129 | } 130 | 131 | public function getRedirectUri(): ?string 132 | { 133 | return $this->authorizationRequest->getRedirectUri(); 134 | } 135 | 136 | public function getState(): ?string 137 | { 138 | return $this->authorizationRequest->getState(); 139 | } 140 | 141 | public function getCodeChallenge(): string 142 | { 143 | return $this->authorizationRequest->getCodeChallenge(); 144 | } 145 | 146 | public function getCodeChallengeMethod(): string 147 | { 148 | return $this->authorizationRequest->getCodeChallengeMethod(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Event/AuthorizationRequestResolveEventFactory.php: -------------------------------------------------------------------------------- 1 | scopeConverter = $scopeConverter; 27 | $this->clientManager = $clientManager; 28 | } 29 | 30 | public function fromAuthorizationRequest(AuthorizationRequest $authorizationRequest): AuthorizationRequestResolveEvent 31 | { 32 | $scopes = $this->scopeConverter->toDomainArray($authorizationRequest->getScopes()); 33 | 34 | $client = $this->clientManager->find($authorizationRequest->getClient()->getIdentifier()); 35 | 36 | if (null === $client) { 37 | throw new RuntimeException(sprintf('No client found for the given identifier \'%s\'.', $authorizationRequest->getClient()->getIdentifier())); 38 | } 39 | 40 | return new AuthorizationRequestResolveEvent($authorizationRequest, $scopes, $client); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Event/ScopeResolveEvent.php: -------------------------------------------------------------------------------- 1 | scopes = $scopes; 37 | $this->grant = $grant; 38 | $this->client = $client; 39 | $this->userIdentifier = $userIdentifier; 40 | } 41 | 42 | /** 43 | * @return Scope[] 44 | */ 45 | public function getScopes(): array 46 | { 47 | return $this->scopes; 48 | } 49 | 50 | public function setScopes(Scope ...$scopes): self 51 | { 52 | $this->scopes = $scopes; 53 | 54 | return $this; 55 | } 56 | 57 | public function getGrant(): Grant 58 | { 59 | return $this->grant; 60 | } 61 | 62 | public function getClient(): Client 63 | { 64 | return $this->client; 65 | } 66 | 67 | public function getUserIdentifier(): ?string 68 | { 69 | return $this->userIdentifier; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Event/UserResolveEvent.php: -------------------------------------------------------------------------------- 1 | username = $username; 35 | $this->password = $password; 36 | $this->grant = $grant; 37 | $this->client = $client; 38 | } 39 | 40 | public function getUsername(): string 41 | { 42 | return $this->username; 43 | } 44 | 45 | public function getPassword(): string 46 | { 47 | return $this->password; 48 | } 49 | 50 | public function getGrant(): Grant 51 | { 52 | return $this->grant; 53 | } 54 | 55 | public function getClient(): Client 56 | { 57 | return $this->client; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /EventListener/AuthorizationRequestUserResolvingListener.php: -------------------------------------------------------------------------------- 1 | security = $security; 24 | } 25 | 26 | public function onAuthorizationRequest(AuthorizationRequestResolveEvent $event): void 27 | { 28 | $user = $this->security->getUser(); 29 | if ($user instanceof UserInterface) { 30 | $event->setUser($user); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /EventListener/ConvertExceptionToResponseListener.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ConvertExceptionToResponseListener 16 | { 17 | public function onKernelException(ExceptionEvent $event): void 18 | { 19 | $exception = $event->getThrowable(); 20 | if ($exception instanceof InsufficientScopesException || $exception instanceof Oauth2AuthenticationFailedException) { 21 | $event->setResponse(new Response($exception->getMessage(), $exception->getCode())); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Trikoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /League/AuthorizationServer/GrantConfigurator.php: -------------------------------------------------------------------------------- 1 | grants = $grants; 19 | } 20 | 21 | public function __invoke(AuthorizationServer $authorizationServer): void 22 | { 23 | foreach ($this->grants as $grant) { 24 | $authorizationServer->enableGrantType($grant, $grant->getAccessTokenTTL()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /League/AuthorizationServer/GrantTypeInterface.php: -------------------------------------------------------------------------------- 1 | getIdentifier(); 27 | } 28 | 29 | /** 30 | * @param string[] $redirectUri 31 | */ 32 | public function setRedirectUri(array $redirectUri): void 33 | { 34 | $this->redirectUri = $redirectUri; 35 | } 36 | 37 | public function setConfidential(bool $isConfidential): void 38 | { 39 | $this->isConfidential = $isConfidential; 40 | } 41 | 42 | public function isPlainTextPkceAllowed(): bool 43 | { 44 | return $this->allowPlainTextPkce; 45 | } 46 | 47 | public function setAllowPlainTextPkce(bool $allowPlainTextPkce): void 48 | { 49 | $this->allowPlainTextPkce = $allowPlainTextPkce; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /League/Entity/RefreshToken.php: -------------------------------------------------------------------------------- 1 | getIdentifier(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /League/Entity/User.php: -------------------------------------------------------------------------------- 1 | accessTokenManager = $accessTokenManager; 40 | $this->clientManager = $clientManager; 41 | $this->scopeConverter = $scopeConverter; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null) 48 | { 49 | $accessToken = new AccessTokenEntity(); 50 | $accessToken->setClient($clientEntity); 51 | $accessToken->setUserIdentifier($userIdentifier); 52 | 53 | foreach ($scopes as $scope) { 54 | $accessToken->addScope($scope); 55 | } 56 | 57 | return $accessToken; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity) 64 | { 65 | $accessToken = $this->accessTokenManager->find($accessTokenEntity->getIdentifier()); 66 | 67 | if (null !== $accessToken) { 68 | throw UniqueTokenIdentifierConstraintViolationException::create(); 69 | } 70 | 71 | $accessToken = $this->buildAccessTokenModel($accessTokenEntity); 72 | 73 | $this->accessTokenManager->save($accessToken); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function revokeAccessToken($tokenId) 80 | { 81 | $accessToken = $this->accessTokenManager->find($tokenId); 82 | 83 | if (null === $accessToken) { 84 | return; 85 | } 86 | 87 | $accessToken->revoke(); 88 | 89 | $this->accessTokenManager->save($accessToken); 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function isAccessTokenRevoked($tokenId) 96 | { 97 | $accessToken = $this->accessTokenManager->find($tokenId); 98 | 99 | if (null === $accessToken) { 100 | return true; 101 | } 102 | 103 | return $accessToken->isRevoked(); 104 | } 105 | 106 | private function buildAccessTokenModel(AccessTokenEntityInterface $accessTokenEntity): AccessTokenModel 107 | { 108 | $client = $this->clientManager->find($accessTokenEntity->getClient()->getIdentifier()); 109 | 110 | return new AccessTokenModel( 111 | $accessTokenEntity->getIdentifier(), 112 | $accessTokenEntity->getExpiryDateTime(), 113 | $client, 114 | $accessTokenEntity->getUserIdentifier(), 115 | $this->scopeConverter->toDomainArray($accessTokenEntity->getScopes()) 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /League/Repository/AuthCodeRepository.php: -------------------------------------------------------------------------------- 1 | authorizationCodeManager = $authorizationCodeManager; 39 | $this->clientManager = $clientManager; 40 | $this->scopeConverter = $scopeConverter; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getNewAuthCode() 47 | { 48 | return new AuthCode(); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function persistNewAuthCode(AuthCodeEntityInterface $authCode) 55 | { 56 | $authorizationCode = $this->authorizationCodeManager->find($authCode->getIdentifier()); 57 | 58 | if (null !== $authorizationCode) { 59 | throw UniqueTokenIdentifierConstraintViolationException::create(); 60 | } 61 | 62 | $authorizationCode = $this->buildAuthorizationCode($authCode); 63 | 64 | $this->authorizationCodeManager->save($authorizationCode); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function revokeAuthCode($codeId) 71 | { 72 | $authorizationCode = $this->authorizationCodeManager->find($codeId); 73 | 74 | if (null === $authorizationCode) { 75 | return; 76 | } 77 | 78 | $authorizationCode->revoke(); 79 | 80 | $this->authorizationCodeManager->save($authorizationCode); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function isAuthCodeRevoked($codeId) 87 | { 88 | $authorizationCode = $this->authorizationCodeManager->find($codeId); 89 | 90 | if (null === $authorizationCode) { 91 | return true; 92 | } 93 | 94 | return $authorizationCode->isRevoked(); 95 | } 96 | 97 | private function buildAuthorizationCode(AuthCode $authCode): AuthorizationCode 98 | { 99 | $client = $this->clientManager->find($authCode->getClient()->getIdentifier()); 100 | 101 | return new AuthorizationCode( 102 | $authCode->getIdentifier(), 103 | $authCode->getExpiryDateTime(), 104 | $client, 105 | $authCode->getUserIdentifier(), 106 | $this->scopeConverter->toDomainArray($authCode->getScopes()) 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /League/Repository/ClientRepository.php: -------------------------------------------------------------------------------- 1 | clientManager = $clientManager; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getClientEntity($clientIdentifier) 28 | { 29 | $client = $this->clientManager->find($clientIdentifier); 30 | 31 | if (null === $client) { 32 | return null; 33 | } 34 | 35 | return $this->buildClientEntity($client); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function validateClient($clientIdentifier, $clientSecret, $grantType) 42 | { 43 | $client = $this->clientManager->find($clientIdentifier); 44 | 45 | if (null === $client) { 46 | return false; 47 | } 48 | 49 | if (!$client->isActive()) { 50 | return false; 51 | } 52 | 53 | if (!$this->isGrantSupported($client, $grantType)) { 54 | return false; 55 | } 56 | 57 | if (!$client->isConfidential() || hash_equals($client->getSecret(), (string) $clientSecret)) { 58 | return true; 59 | } 60 | 61 | return false; 62 | } 63 | 64 | private function buildClientEntity(ClientModel $client): ClientEntity 65 | { 66 | $clientEntity = new ClientEntity(); 67 | $clientEntity->setIdentifier($client->getIdentifier()); 68 | $clientEntity->setRedirectUri(array_map('strval', $client->getRedirectUris())); 69 | $clientEntity->setConfidential($client->isConfidential()); 70 | $clientEntity->setAllowPlainTextPkce($client->isPlainTextPkceAllowed()); 71 | 72 | return $clientEntity; 73 | } 74 | 75 | private function isGrantSupported(ClientModel $client, ?string $grant): bool 76 | { 77 | if (null === $grant) { 78 | return true; 79 | } 80 | 81 | $grants = $client->getGrants(); 82 | 83 | if (empty($grants)) { 84 | return true; 85 | } 86 | 87 | return \in_array($grant, $client->getGrants()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /League/Repository/RefreshTokenRepository.php: -------------------------------------------------------------------------------- 1 | refreshTokenManager = $refreshTokenManager; 32 | $this->accessTokenManager = $accessTokenManager; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getNewRefreshToken() 39 | { 40 | return new RefreshTokenEntity(); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity) 47 | { 48 | $refreshToken = $this->refreshTokenManager->find($refreshTokenEntity->getIdentifier()); 49 | 50 | if (null !== $refreshToken) { 51 | throw UniqueTokenIdentifierConstraintViolationException::create(); 52 | } 53 | 54 | $refreshToken = $this->buildRefreshTokenModel($refreshTokenEntity); 55 | 56 | $this->refreshTokenManager->save($refreshToken); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function revokeRefreshToken($tokenId) 63 | { 64 | $refreshToken = $this->refreshTokenManager->find($tokenId); 65 | 66 | if (null === $refreshToken) { 67 | return; 68 | } 69 | 70 | $refreshToken->revoke(); 71 | 72 | $this->refreshTokenManager->save($refreshToken); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function isRefreshTokenRevoked($tokenId) 79 | { 80 | $refreshToken = $this->refreshTokenManager->find($tokenId); 81 | 82 | if (null === $refreshToken) { 83 | return true; 84 | } 85 | 86 | return $refreshToken->isRevoked(); 87 | } 88 | 89 | private function buildRefreshTokenModel(RefreshTokenEntityInterface $refreshTokenEntity): RefreshTokenModel 90 | { 91 | $accessToken = $this->accessTokenManager->find($refreshTokenEntity->getAccessToken()->getIdentifier()); 92 | 93 | return new RefreshTokenModel( 94 | $refreshTokenEntity->getIdentifier(), 95 | $refreshTokenEntity->getExpiryDateTime(), 96 | $accessToken 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /League/Repository/ScopeRepository.php: -------------------------------------------------------------------------------- 1 | scopeManager = $scopeManager; 49 | $this->clientManager = $clientManager; 50 | $this->scopeConverter = $scopeConverter; 51 | $this->eventDispatcher = $eventDispatcher; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getScopeEntityByIdentifier($identifier) 58 | { 59 | $scope = $this->scopeManager->find($identifier); 60 | 61 | if (null === $scope) { 62 | return null; 63 | } 64 | 65 | return $this->scopeConverter->toLeague($scope); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function finalizeScopes( 72 | array $scopes, 73 | $grantType, 74 | ClientEntityInterface $clientEntity, 75 | $userIdentifier = null 76 | ) { 77 | $client = $this->clientManager->find($clientEntity->getIdentifier()); 78 | 79 | $scopes = $this->setupScopes($client, $this->scopeConverter->toDomainArray($scopes)); 80 | 81 | $event = $this->eventDispatcher->dispatch( 82 | new ScopeResolveEvent( 83 | $scopes, 84 | new GrantModel($grantType), 85 | $client, 86 | $userIdentifier 87 | ), 88 | OAuth2Events::SCOPE_RESOLVE 89 | ); 90 | 91 | return $this->scopeConverter->toLeagueArray($event->getScopes()); 92 | } 93 | 94 | /** 95 | * @param ScopeModel[] $requestedScopes 96 | * 97 | * @return ScopeModel[] 98 | */ 99 | private function setupScopes(ClientModel $client, array $requestedScopes): array 100 | { 101 | $clientScopes = $client->getScopes(); 102 | 103 | if (empty($clientScopes)) { 104 | return $requestedScopes; 105 | } 106 | 107 | if (empty($requestedScopes)) { 108 | return $clientScopes; 109 | } 110 | 111 | $finalizedScopes = []; 112 | $clientScopesAsStrings = array_map('strval', $clientScopes); 113 | 114 | foreach ($requestedScopes as $requestedScope) { 115 | $requestedScopeAsString = (string) $requestedScope; 116 | if (!\in_array($requestedScopeAsString, $clientScopesAsStrings, true)) { 117 | throw OAuthServerException::invalidScope($requestedScopeAsString); 118 | } 119 | 120 | $finalizedScopes[] = $requestedScope; 121 | } 122 | 123 | return $finalizedScopes; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /League/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | clientManager = $clientManager; 39 | $this->eventDispatcher = $eventDispatcher; 40 | $this->userConverter = $userConverter; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getUserEntityByUserCredentials( 47 | $username, 48 | $password, 49 | $grantType, 50 | ClientEntityInterface $clientEntity 51 | ) { 52 | $client = $this->clientManager->find($clientEntity->getIdentifier()); 53 | 54 | $event = $this->eventDispatcher->dispatch( 55 | new UserResolveEvent( 56 | $username, 57 | $password, 58 | new GrantModel($grantType), 59 | $client 60 | ), 61 | OAuth2Events::USER_RESOLVE 62 | ); 63 | 64 | $user = $event->getUser(); 65 | 66 | if (null === $user) { 67 | return null; 68 | } 69 | 70 | return $this->userConverter->toLeague($user); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Manager/AccessTokenManagerInterface.php: -------------------------------------------------------------------------------- 1 | addCriteria($this->grants, ...$grants); 36 | } 37 | 38 | public function addRedirectUriCriteria(RedirectUri ...$redirectUris): self 39 | { 40 | return $this->addCriteria($this->redirectUris, ...$redirectUris); 41 | } 42 | 43 | public function addScopeCriteria(Scope ...$scopes): self 44 | { 45 | return $this->addCriteria($this->scopes, ...$scopes); 46 | } 47 | 48 | private function addCriteria(&$field, ...$values): self 49 | { 50 | if (0 === \count($values)) { 51 | return $this; 52 | } 53 | 54 | $field = array_merge($field, $values); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return Grant[] 61 | */ 62 | public function getGrants(): array 63 | { 64 | return $this->grants; 65 | } 66 | 67 | /** 68 | * @return RedirectUri[] 69 | */ 70 | public function getRedirectUris(): array 71 | { 72 | return $this->redirectUris; 73 | } 74 | 75 | /** 76 | * @return Scope[] 77 | */ 78 | public function getScopes(): array 79 | { 80 | return $this->scopes; 81 | } 82 | 83 | public function hasFilters(): bool 84 | { 85 | return 86 | !empty($this->grants) 87 | || !empty($this->redirectUris) 88 | || !empty($this->scopes); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Manager/ClientManagerInterface.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function find(string $identifier): ?AccessToken 28 | { 29 | return $this->entityManager->find(AccessToken::class, $identifier); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function save(AccessToken $accessToken): void 36 | { 37 | $this->entityManager->persist($accessToken); 38 | $this->entityManager->flush(); 39 | } 40 | 41 | public function clearExpired(): int 42 | { 43 | return $this->entityManager->createQueryBuilder() 44 | ->delete(AccessToken::class, 'at') 45 | ->where('at.expiry < :expiry') 46 | ->setParameter('expiry', new DateTimeImmutable()) 47 | ->getQuery() 48 | ->execute(); 49 | } 50 | 51 | public function clearRevoked(): int 52 | { 53 | return $this->entityManager->createQueryBuilder() 54 | ->delete(AccessToken::class, 'at') 55 | ->where('at.revoked = true') 56 | ->getQuery() 57 | ->execute(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Manager/Doctrine/AuthorizationCodeManager.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function find(string $identifier): ?AuthorizationCode 28 | { 29 | return $this->entityManager->find(AuthorizationCode::class, $identifier); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function save(AuthorizationCode $authorizationCode): void 36 | { 37 | $this->entityManager->persist($authorizationCode); 38 | $this->entityManager->flush(); 39 | } 40 | 41 | public function clearExpired(): int 42 | { 43 | return $this->entityManager->createQueryBuilder() 44 | ->delete(AuthorizationCode::class, 'ac') 45 | ->where('ac.expiry < :expiry') 46 | ->setParameter('expiry', new DateTimeImmutable()) 47 | ->getQuery() 48 | ->execute(); 49 | } 50 | 51 | public function clearRevoked(): int 52 | { 53 | return $this->entityManager->createQueryBuilder() 54 | ->delete(AuthorizationCode::class, 'ac') 55 | ->where('ac.revoked = true') 56 | ->getQuery() 57 | ->execute(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Manager/Doctrine/ClientManager.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function find(string $identifier): ?Client 28 | { 29 | return $this->entityManager->find(Client::class, $identifier); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function save(Client $client): void 36 | { 37 | $this->entityManager->persist($client); 38 | $this->entityManager->flush(); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function remove(Client $client): void 45 | { 46 | $this->entityManager->remove($client); 47 | $this->entityManager->flush(); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function list(?ClientFilter $clientFilter): array 54 | { 55 | $repository = $this->entityManager->getRepository(Client::class); 56 | $criteria = self::filterToCriteria($clientFilter); 57 | 58 | return $repository->findBy($criteria); 59 | } 60 | 61 | private static function filterToCriteria(?ClientFilter $clientFilter): array 62 | { 63 | if (null === $clientFilter || false === $clientFilter->hasFilters()) { 64 | return []; 65 | } 66 | 67 | $criteria = []; 68 | 69 | $grants = $clientFilter->getGrants(); 70 | if ($grants) { 71 | $criteria['grants'] = $grants; 72 | } 73 | 74 | $redirectUris = $clientFilter->getRedirectUris(); 75 | if ($redirectUris) { 76 | $criteria['redirect_uris'] = $redirectUris; 77 | } 78 | 79 | $scopes = $clientFilter->getScopes(); 80 | if ($scopes) { 81 | $criteria['scopes'] = $scopes; 82 | } 83 | 84 | return $criteria; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Manager/Doctrine/RefreshTokenManager.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function find(string $identifier): ?RefreshToken 28 | { 29 | return $this->entityManager->find(RefreshToken::class, $identifier); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function save(RefreshToken $refreshToken): void 36 | { 37 | $this->entityManager->persist($refreshToken); 38 | $this->entityManager->flush(); 39 | } 40 | 41 | public function clearExpired(): int 42 | { 43 | return $this->entityManager->createQueryBuilder() 44 | ->delete(RefreshToken::class, 'rt') 45 | ->where('rt.expiry < :expiry') 46 | ->setParameter('expiry', new DateTimeImmutable()) 47 | ->getQuery() 48 | ->execute(); 49 | } 50 | 51 | public function clearRevoked(): int 52 | { 53 | return $this->entityManager->createQueryBuilder() 54 | ->delete(RefreshToken::class, 'rt') 55 | ->where('rt.revoked = true') 56 | ->getQuery() 57 | ->execute(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Manager/InMemory/AccessTokenManager.php: -------------------------------------------------------------------------------- 1 | accessTokens[$identifier] ?? null; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function save(AccessToken $accessToken): void 30 | { 31 | $this->accessTokens[$accessToken->getIdentifier()] = $accessToken; 32 | } 33 | 34 | public function clearExpired(): int 35 | { 36 | $count = \count($this->accessTokens); 37 | 38 | $now = new DateTimeImmutable(); 39 | $this->accessTokens = array_filter($this->accessTokens, static function (AccessToken $accessToken) use ($now): bool { 40 | return $accessToken->getExpiry() >= $now; 41 | }); 42 | 43 | return $count - \count($this->accessTokens); 44 | } 45 | 46 | public function clearRevoked(): int 47 | { 48 | $count = \count($this->accessTokens); 49 | 50 | $this->accessTokens = array_filter($this->accessTokens, static function (AccessToken $accessToken): bool { 51 | return !$accessToken->isRevoked(); 52 | }); 53 | 54 | return $count - \count($this->accessTokens); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Manager/InMemory/AuthorizationCodeManager.php: -------------------------------------------------------------------------------- 1 | authorizationCodes[$identifier] ?? null; 21 | } 22 | 23 | public function save(AuthorizationCode $authorizationCode): void 24 | { 25 | $this->authorizationCodes[$authorizationCode->getIdentifier()] = $authorizationCode; 26 | } 27 | 28 | public function clearExpired(): int 29 | { 30 | $count = \count($this->authorizationCodes); 31 | 32 | $now = new DateTimeImmutable(); 33 | $this->authorizationCodes = array_filter($this->authorizationCodes, static function (AuthorizationCode $authorizationCode) use ($now): bool { 34 | return $authorizationCode->getExpiryDateTime() >= $now; 35 | }); 36 | 37 | return $count - \count($this->authorizationCodes); 38 | } 39 | 40 | public function clearRevoked(): int 41 | { 42 | $count = \count($this->authorizationCodes); 43 | 44 | $this->authorizationCodes = array_filter($this->authorizationCodes, static function (AuthorizationCode $authorizationCode): bool { 45 | return !$authorizationCode->isRevoked(); 46 | }); 47 | 48 | return $count - \count($this->authorizationCodes); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Manager/InMemory/ClientManager.php: -------------------------------------------------------------------------------- 1 | clients[$identifier] ?? null; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function save(Client $client): void 30 | { 31 | $this->clients[$client->getIdentifier()] = $client; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function remove(Client $client): void 38 | { 39 | unset($this->clients[$client->getIdentifier()]); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function list(?ClientFilter $clientFilter): array 46 | { 47 | if (!$clientFilter || !$clientFilter->hasFilters()) { 48 | return $this->clients; 49 | } 50 | 51 | return array_filter($this->clients, static function (Client $client) use ($clientFilter): bool { 52 | $grantsPassed = self::passesFilter($client->getGrants(), $clientFilter->getGrants()); 53 | $scopesPassed = self::passesFilter($client->getScopes(), $clientFilter->getScopes()); 54 | $redirectUrisPassed = self::passesFilter($client->getRedirectUris(), $clientFilter->getRedirectUris()); 55 | 56 | return $grantsPassed && $scopesPassed && $redirectUrisPassed; 57 | }); 58 | } 59 | 60 | private static function passesFilter(array $clientValues, array $filterValues): bool 61 | { 62 | if (empty($filterValues)) { 63 | return true; 64 | } 65 | 66 | $clientValues = array_map('strval', $clientValues); 67 | $filterValues = array_map('strval', $filterValues); 68 | 69 | $valuesPassed = array_intersect($filterValues, $clientValues); 70 | 71 | return \count($valuesPassed) > 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Manager/InMemory/RefreshTokenManager.php: -------------------------------------------------------------------------------- 1 | refreshTokens[$identifier] ?? null; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function save(RefreshToken $refreshToken): void 30 | { 31 | $this->refreshTokens[$refreshToken->getIdentifier()] = $refreshToken; 32 | } 33 | 34 | public function clearExpired(): int 35 | { 36 | $count = \count($this->refreshTokens); 37 | 38 | $now = new DateTimeImmutable(); 39 | $this->refreshTokens = array_filter($this->refreshTokens, static function (RefreshToken $refreshToken) use ($now): bool { 40 | return $refreshToken->getExpiry() >= $now; 41 | }); 42 | 43 | return $count - \count($this->refreshTokens); 44 | } 45 | 46 | public function clearRevoked(): int 47 | { 48 | $count = \count($this->refreshTokens); 49 | 50 | $this->refreshTokens = array_filter($this->refreshTokens, static function (RefreshToken $refreshToken): bool { 51 | return !$refreshToken->isRevoked(); 52 | }); 53 | 54 | return $count - \count($this->refreshTokens); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Manager/InMemory/ScopeManager.php: -------------------------------------------------------------------------------- 1 | scopes[$identifier] ?? null; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function save(Scope $scope): void 29 | { 30 | $this->scopes[(string) $scope] = $scope; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Manager/RefreshTokenManagerInterface.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 49 | $this->expiry = $expiry; 50 | $this->client = $client; 51 | $this->userIdentifier = $userIdentifier; 52 | $this->scopes = $scopes; 53 | } 54 | 55 | public function __toString(): string 56 | { 57 | return $this->getIdentifier(); 58 | } 59 | 60 | public function getIdentifier(): string 61 | { 62 | return $this->identifier; 63 | } 64 | 65 | public function getExpiry(): DateTimeInterface 66 | { 67 | return $this->expiry; 68 | } 69 | 70 | public function getUserIdentifier(): ?string 71 | { 72 | return $this->userIdentifier; 73 | } 74 | 75 | public function getClient(): Client 76 | { 77 | return $this->client; 78 | } 79 | 80 | /** 81 | * @return Scope[] 82 | */ 83 | public function getScopes(): array 84 | { 85 | return $this->scopes; 86 | } 87 | 88 | public function isRevoked(): bool 89 | { 90 | return $this->revoked; 91 | } 92 | 93 | public function revoke(): self 94 | { 95 | $this->revoked = true; 96 | 97 | return $this; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Model/AuthorizationCode.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 49 | $this->expiry = $expiry; 50 | $this->client = $client; 51 | $this->userIdentifier = $userIdentifier; 52 | $this->scopes = $scopes; 53 | } 54 | 55 | public function __toString(): string 56 | { 57 | return $this->getIdentifier(); 58 | } 59 | 60 | public function getIdentifier(): string 61 | { 62 | return $this->identifier; 63 | } 64 | 65 | public function getExpiryDateTime(): DateTimeInterface 66 | { 67 | return $this->expiry; 68 | } 69 | 70 | public function getUserIdentifier(): ?string 71 | { 72 | return $this->userIdentifier; 73 | } 74 | 75 | public function getClient(): Client 76 | { 77 | return $this->client; 78 | } 79 | 80 | /** 81 | * @return Scope[] 82 | */ 83 | public function getScopes(): array 84 | { 85 | return $this->scopes; 86 | } 87 | 88 | public function isRevoked(): bool 89 | { 90 | return $this->revoked; 91 | } 92 | 93 | public function revoke(): self 94 | { 95 | $this->revoked = true; 96 | 97 | return $this; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Model/Client.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 47 | $this->secret = $secret; 48 | } 49 | 50 | public function __toString(): string 51 | { 52 | return $this->getIdentifier(); 53 | } 54 | 55 | public function getIdentifier(): string 56 | { 57 | return $this->identifier; 58 | } 59 | 60 | public function getSecret(): ?string 61 | { 62 | return $this->secret; 63 | } 64 | 65 | public function setSecret(?string $secret): self 66 | { 67 | $this->secret = $secret; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * @return RedirectUri[] 74 | */ 75 | public function getRedirectUris(): array 76 | { 77 | return $this->redirectUris; 78 | } 79 | 80 | public function setRedirectUris(RedirectUri ...$redirectUris): self 81 | { 82 | $this->redirectUris = $redirectUris; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @return Grant[] 89 | */ 90 | public function getGrants(): array 91 | { 92 | return $this->grants; 93 | } 94 | 95 | public function setGrants(Grant ...$grants): self 96 | { 97 | $this->grants = $grants; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * @return Scope[] 104 | */ 105 | public function getScopes(): array 106 | { 107 | return $this->scopes; 108 | } 109 | 110 | public function setScopes(Scope ...$scopes): self 111 | { 112 | $this->scopes = $scopes; 113 | 114 | return $this; 115 | } 116 | 117 | public function isActive(): bool 118 | { 119 | return $this->active; 120 | } 121 | 122 | public function setActive(bool $active): self 123 | { 124 | $this->active = $active; 125 | 126 | return $this; 127 | } 128 | 129 | public function isConfidential(): bool 130 | { 131 | return !empty($this->secret); 132 | } 133 | 134 | public function isPlainTextPkceAllowed(): bool 135 | { 136 | return $this->allowPlainTextPkce; 137 | } 138 | 139 | public function setAllowPlainTextPkce(bool $allowPlainTextPkce): self 140 | { 141 | $this->allowPlainTextPkce = $allowPlainTextPkce; 142 | 143 | return $this; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Model/Grant.php: -------------------------------------------------------------------------------- 1 | grant = $grant; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return $this->grant; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Model/RedirectUri.php: -------------------------------------------------------------------------------- 1 | assertValidRedirectUri($redirectUri); 19 | 20 | $this->redirectUri = $redirectUri; 21 | } 22 | 23 | private function assertValidRedirectUri(string $redirectUri): void 24 | { 25 | if (filter_var($redirectUri, \FILTER_VALIDATE_URL)) { 26 | return; 27 | } 28 | 29 | if ($this->isValidCustomURIScheme($redirectUri)) { 30 | return; 31 | } 32 | 33 | throw new RuntimeException(sprintf('The \'%s\' string is not a valid URI.', $redirectUri)); 34 | } 35 | 36 | /** 37 | * @see https://developers.google.com/identity/protocols/oauth2/native-app?hl=en 38 | */ 39 | private function isValidCustomURIScheme(string $redirectUri): bool 40 | { 41 | $parts = parse_url($redirectUri); 42 | if (isset($parts['host'])) { 43 | return false; 44 | } 45 | if (!isset($parts['scheme'])) { 46 | return false; 47 | } 48 | 49 | //Deny if scheme start or end by "." 50 | if ('.' === substr($parts['scheme'], 0, 1) || '.' === substr($parts['scheme'], -1, 1)) { 51 | return false; 52 | } 53 | 54 | //Deny if scheme doesn't have "." domain separator 55 | if (false === strpos(substr($parts['scheme'], 1, -1), '.')) { 56 | return false; 57 | } 58 | 59 | if (isset($parts['path'])) { 60 | if ('/' !== $parts['path'][0]) { 61 | return false; 62 | } 63 | if (1 < \strlen($parts['path']) && '/' === $parts['path'][1]) { 64 | return false; 65 | } 66 | } 67 | 68 | return true; 69 | } 70 | 71 | public function __toString(): string 72 | { 73 | return $this->redirectUri; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Model/RefreshToken.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 34 | $this->expiry = $expiry; 35 | $this->accessToken = $accessToken; 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | return $this->getIdentifier(); 41 | } 42 | 43 | public function getIdentifier(): string 44 | { 45 | return $this->identifier; 46 | } 47 | 48 | public function getExpiry(): DateTimeInterface 49 | { 50 | return $this->expiry; 51 | } 52 | 53 | public function getAccessToken(): ?AccessToken 54 | { 55 | return $this->accessToken; 56 | } 57 | 58 | public function isRevoked(): bool 59 | { 60 | return $this->revoked; 61 | } 62 | 63 | public function revoke(): self 64 | { 65 | $this->revoked = true; 66 | 67 | return $this; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Model/Scope.php: -------------------------------------------------------------------------------- 1 | scope = $scope; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return $this->scope; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /OAuth2Events.php: -------------------------------------------------------------------------------- 1 | 'authorization code', 52 | self::CLIENT_CREDENTIALS => 'client credentials', 53 | self::IMPLICIT => 'implicit', 54 | self::PASSWORD => 'password', 55 | self::REFRESH_TOKEN => 'refresh token', 56 | ]; 57 | 58 | /** 59 | * @deprecated Will be removed in v4, use {@see OAuth2Grants::ALL} instead 60 | * 61 | * @TODO Remove in v4. 62 | */ 63 | public static function has(string $grant): bool 64 | { 65 | return isset(self::ALL[$grant]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trikoder OAuth 2 Bundle 2 | 3 | [![Build Status](https://github.com/trikoder/oauth2-bundle/workflows/Tests/badge.svg?branch=v3.x)](https://github.com/trikoder/oauth2-bundle/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/trikoder/oauth2-bundle/v/stable)](https://packagist.org/packages/trikoder/oauth2-bundle) 5 | [![License](https://poser.pugx.org/trikoder/oauth2-bundle/license)](https://packagist.org/packages/trikoder/oauth2-bundle) 6 | [![Code coverage](https://codecov.io/gh/trikoder/oauth2-bundle/branch/master/graph/badge.svg)](https://codecov.io/gh/trikoder/oauth2-bundle) 7 | 8 | Symfony bundle which provides OAuth 2.0 authorization/resource server capabilities. The authorization and resource server actors are implemented using the [thephpleague/oauth2-server](https://github.com/thephpleague/oauth2-server) library. 9 | 10 | ## Important notes 11 | 12 | This bundle provides the "glue" between [thephpleague/oauth2-server](https://github.com/thephpleague/oauth2-server) library and Symfony. 13 | It implements [thephpleague/oauth2-server](https://github.com/thephpleague/oauth2-server) library in a way specified by its official documentation. 14 | For implementation into Symfony project, please see [bundle documentation](docs/basic-setup.md) and official [Symfony security documentation](https://symfony.com/doc/current/security.html). 15 | 16 | ## Status ⚠️ 17 | 18 | Active development is currently on hold, as this repository is in progress of migrating to the [thephpleague/oauth2-server-bundle](https://github.com/thephpleague/oauth2-server-bundle) project. 19 | 20 | *The current repository will be **discontinued** whenever the `v1.0` release is ready in [oauth2-server-bundle](https://github.com/thephpleague/oauth2-server-bundle/releases).* 21 | *See [this comment](https://github.com/trikoder/oauth2-bundle/pull/292#issuecomment-990943939) for more information.* 22 | 23 | ## Features 24 | 25 | * API endpoint for client authorization and token issuing 26 | * Configurable client and token persistance (includes [Doctrine](https://www.doctrine-project.org/) support) 27 | * Integration with Symfony's [Security](https://symfony.com/doc/current/security.html) layer 28 | 29 | ## Requirements 30 | 31 | * [PHP 7.2](http://php.net/releases/7_2_0.php) or greater 32 | * [Symfony 4.4](https://symfony.com/roadmap/4.4) or [Symfony 5.x](https://symfony.com/roadmap/5.0) 33 | 34 | ## Installation 35 | 36 | 1. Require the bundle and a PSR 7/17 implementation with Composer: 37 | 38 | ```sh 39 | composer require trikoder/oauth2-bundle nyholm/psr7 40 | ``` 41 | 42 | If your project is managed using [Symfony Flex](https://github.com/symfony/flex), the rest of the steps are not required. Just follow the post-installation instructions instead! :tada: 43 | 44 | > **NOTE:** This bundle requires a PSR 7/17 implementation to operate. We recommend that you use [nyholm/psr7](https://github.com/Nyholm/psr7). Check out this [document](docs/psr-implementation-switching.md) if you wish to use a different implementation. 45 | 46 | 1. Create the bundle configuration file under `config/packages/trikoder_oauth2.yaml`. Here is a reference configuration file: 47 | 48 | ```yaml 49 | trikoder_oauth2: 50 | authorization_server: # Required 51 | 52 | # Full path to the private key file. 53 | # How to generate a private key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys 54 | private_key: ~ # Required, Example: /var/oauth/private.key 55 | 56 | # Passphrase of the private key, if any. 57 | private_key_passphrase: null 58 | 59 | # The plain string or the ascii safe string used to create a Defuse\Crypto\Key to be used as an encryption key. 60 | # How to generate an encryption key: https://oauth2.thephpleague.com/installation/#string-password 61 | encryption_key: ~ # Required 62 | 63 | # The type of value of "encryption_key". 64 | encryption_key_type: plain # One of "plain"; "defuse" 65 | 66 | # How long the issued access token should be valid for, used as a default if there is no grant type specific value set. 67 | # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters 68 | access_token_ttl: PT1H 69 | 70 | # How long the issued refresh token should be valid for, used as a default if there is no grant type specific value set. 71 | # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters 72 | refresh_token_ttl: P1M 73 | 74 | # How long the issued authorization code should be valid for. 75 | # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters 76 | auth_code_ttl: ~ # Deprecated ("trikoder_oauth2.authorization_server.auth_code_ttl" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.authorization_code.auth_code_ttl" instead.) 77 | 78 | # Whether to require code challenge for public clients for the authorization code grant. 79 | require_code_challenge_for_public_clients: ~ # Deprecated ("trikoder_oauth2.authorization_server.require_code_challenge_for_public_clients" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.authorization_code.require_code_challenge_for_public_clients" instead.) 80 | 81 | # Whether to enable the authorization code grant. 82 | enable_auth_code_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_auth_code_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.authorization_code.enable" instead.) 83 | 84 | # Whether to enable the client credentials grant. 85 | enable_client_credentials_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_client_credentials_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.client_credentials.enable" instead.) 86 | 87 | # Whether to enable the implicit grant. 88 | enable_implicit_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_implicit_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.implicit.enable" instead.) 89 | 90 | # Whether to enable the password grant. 91 | enable_password_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_password_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.password.enable" instead.) 92 | 93 | # Whether to enable the refresh token grant. 94 | enable_refresh_token_grant: ~ # Deprecated ("trikoder_oauth2.authorization_server.enable_refresh_token_grant" is deprecated, use "trikoder_oauth2.authorization_server.grant_types.refresh_token.enable" instead.) 95 | 96 | # Enable and configure grant types. 97 | grant_types: 98 | authorization_code: 99 | 100 | # Whether to enable the authorization code grant. 101 | enable: true 102 | 103 | # How long the issued access token should be valid for the authorization code grant. 104 | access_token_ttl: ~ 105 | 106 | # How long the issued refresh token should be valid for the authorization code grant. 107 | refresh_token_ttl: ~ 108 | 109 | # How long the issued authorization code should be valid for. 110 | # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters 111 | auth_code_ttl: PT10M 112 | 113 | # Whether to require code challenge for public clients for the authorization code grant. 114 | require_code_challenge_for_public_clients: true 115 | client_credentials: 116 | 117 | # Whether to enable the client credentials grant. 118 | enable: true 119 | 120 | # How long the issued access token should be valid for the client credentials grant. 121 | access_token_ttl: ~ 122 | implicit: 123 | 124 | # Whether to enable the implicit grant. 125 | enable: true 126 | 127 | # How long the issued access token should be valid for the implicit grant. 128 | access_token_ttl: ~ 129 | password: 130 | 131 | # Whether to enable the password grant. 132 | enable: true 133 | 134 | # How long the issued access token should be valid for the password grant. 135 | access_token_ttl: ~ 136 | 137 | # How long the issued refresh token should be valid for the password grant. 138 | refresh_token_ttl: ~ 139 | refresh_token: 140 | 141 | # Whether to enable the refresh token grant. 142 | enable: true 143 | 144 | # How long the issued access token should be valid for the refresh token grant. 145 | access_token_ttl: ~ 146 | 147 | # How long the issued refresh token should be valid for the refresh token grant. 148 | refresh_token_ttl: ~ 149 | resource_server: # Required 150 | 151 | # Full path to the public key file. 152 | # How to generate a public key: https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys 153 | public_key: ~ # Required, Example: /var/oauth/public.key 154 | 155 | # Scopes that you wish to utilize in your application. 156 | # This should be a simple array of strings. 157 | scopes: [] 158 | 159 | # Configures different persistence methods that can be used by the bundle for saving client and token data. 160 | # Only one persistence method can be configured at a time. 161 | persistence: # Required 162 | doctrine: 163 | 164 | # Name of the entity manager that you wish to use for managing clients and tokens. 165 | entity_manager: default 166 | in_memory: ~ 167 | 168 | # The priority of the event listener that converts an Exception to a Response. 169 | exception_event_listener_priority: 10 170 | 171 | # Set a custom prefix that replaces the default "ROLE_OAUTH2_" role prefix. 172 | role_prefix: ROLE_OAUTH2_ 173 | ``` 174 | 175 | 1. Enable the bundle in `config/bundles.php` by adding it to the array: 176 | 177 | ```php 178 | Trikoder\Bundle\OAuth2Bundle\TrikoderOAuth2Bundle::class => ['all' => true] 179 | ``` 180 | 181 | 1. Update the database so bundle entities can be persisted using Doctrine: 182 | 183 | ```sh 184 | bin/console doctrine:schema:update --force 185 | ``` 186 | 187 | 1. Import the routes inside your `config/routes.yaml` file: 188 | 189 | ```yaml 190 | oauth2: 191 | resource: '@TrikoderOAuth2Bundle/Resources/config/routes.xml' 192 | ``` 193 | 194 | You can verify that everything is working by issuing a `POST` request to the `/token` endpoint. 195 | 196 | **❮ NOTE ❯** It is recommended to control the access to the authorization endpoint 197 | so that only logged in users can approve authorization requests. 198 | You should review your `security.yml` file. Here is a sample configuration: 199 | 200 | ```yaml 201 | security: 202 | access_control: 203 | - { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED } 204 | ``` 205 | 206 | ## Configuration 207 | 208 | * [Basic setup](docs/basic-setup.md) 209 | * [Controlling token scopes](docs/controlling-token-scopes.md) 210 | * [Password grant handling](docs/password-grant-handling.md) 211 | * [Implementing custom grant type](docs/implementing-custom-grant-type.md) 212 | 213 | ## Contributing 214 | 215 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 216 | 217 | ## Versioning 218 | 219 | This project adheres to [Semantic Versioning 2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 220 | 221 | However, starting with version 4, we only promise to follow SemVer on structural elements marked with the [@api tag](https://github.com/php-fig/fig-standards/blob/2668020622d9d9eaf11d403bc1d26664dfc3ef8e/proposed/phpdoc-tags.md#51-api). 222 | 223 | ## Changes 224 | 225 | All the package releases are recorded in the [CHANGELOG](CHANGELOG.md) file. 226 | 227 | ## Reporting issues 228 | 229 | Use the [issue tracker](https://github.com/trikoder/oauth2-bundle/issues) to report any issues you might have. 230 | 231 | ## License 232 | 233 | See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). 234 | -------------------------------------------------------------------------------- /Resources/config/doctrine/model/AccessToken.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Resources/config/doctrine/model/AuthorizationCode.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Resources/config/doctrine/model/Client.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Resources/config/doctrine/model/RefreshToken.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/config/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Resources/config/storage/doctrine.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | The "%alias_id%" service alias is deprecated and will be removed in v4. 25 | 26 | 27 | 28 | 29 | 30 | 31 | The "%alias_id%" service alias is deprecated and will be removed in v4. 32 | 33 | 34 | 35 | 36 | 37 | 38 | The "%alias_id%" service alias is deprecated and will be removed in v4. 39 | 40 | 41 | 42 | 43 | The "%alias_id%" service alias is deprecated and will be removed in v4. 44 | 45 | 46 | 47 | 48 | 49 | 50 | The "%alias_id%" service alias is deprecated and will be removed in v4. 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Resources/config/storage/in_memory.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | The "%alias_id%" service alias is deprecated and will be removed in v4. 18 | 19 | 20 | 21 | 22 | The "%alias_id%" service alias is deprecated and will be removed in v4. 23 | 24 | 25 | 26 | 27 | The "%alias_id%" service alias is deprecated and will be removed in v4. 28 | 29 | 30 | 31 | 32 | The "%alias_id%" service alias is deprecated and will be removed in v4. 33 | 34 | 35 | 36 | 37 | The "%alias_id%" service alias is deprecated and will be removed in v4. 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Security/Authentication/Provider/OAuth2Provider.php: -------------------------------------------------------------------------------- 1 | userProvider = $userProvider; 47 | $this->resourceServer = $resourceServer; 48 | $this->oauth2TokenFactory = $oauth2TokenFactory; 49 | $this->providerKey = $providerKey; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function authenticate(TokenInterface $token) 56 | { 57 | if (!$this->supports($token)) { 58 | throw new RuntimeException(sprintf('This authentication provider can only handle tokes of type \'%s\'.', OAuth2Token::class)); 59 | } 60 | 61 | try { 62 | $request = $this->resourceServer->validateAuthenticatedRequest( 63 | $token->getAttribute('server_request') 64 | ); 65 | } catch (OAuthServerException $e) { 66 | throw new AuthenticationException('The resource server rejected the request.', 0, $e); 67 | } 68 | 69 | $user = $this->getAuthenticatedUser( 70 | $request->getAttribute('oauth_user_id') 71 | ); 72 | 73 | $token = $this->oauth2TokenFactory->createOAuth2Token($request, $user, $this->providerKey); 74 | $token->setAuthenticated(true); 75 | 76 | return $token; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function supports(TokenInterface $token) 83 | { 84 | return $token instanceof OAuth2Token && $this->providerKey === $token->getProviderKey(); 85 | } 86 | 87 | private function getAuthenticatedUser(string $userIdentifier): ?UserInterface 88 | { 89 | if ('' === $userIdentifier) { 90 | /* 91 | * If the identifier is an empty string, that means that the 92 | * access token isn't bound to a user defined in the system. 93 | */ 94 | return null; 95 | } 96 | 97 | return $this->userProvider->loadUserByUsername($userIdentifier); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Security/Authentication/Token/OAuth2Token.php: -------------------------------------------------------------------------------- 1 | setAttribute('server_request', $serverRequest); 26 | $this->setAttribute('role_prefix', $rolePrefix); 27 | 28 | $roles = $this->buildRolesFromScopes(); 29 | 30 | if (null !== $user) { 31 | // Merge the user's roles with the OAuth 2.0 scopes. 32 | $roles = array_merge($roles, $user->getRoles()); 33 | 34 | $this->setUser($user); 35 | } 36 | 37 | parent::__construct(array_unique($roles)); 38 | 39 | $this->providerKey = $providerKey; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getCredentials() 46 | { 47 | return $this->getAttribute('server_request')->getAttribute('oauth_access_token_id'); 48 | } 49 | 50 | public function getProviderKey(): string 51 | { 52 | return $this->providerKey; 53 | } 54 | 55 | public function __serialize(): array 56 | { 57 | return [$this->providerKey, parent::__serialize()]; 58 | } 59 | 60 | public function __unserialize(array $data): void 61 | { 62 | [$this->providerKey, $parentData] = $data; 63 | parent::__unserialize($parentData); 64 | } 65 | 66 | private function buildRolesFromScopes(): array 67 | { 68 | $prefix = $this->getAttribute('role_prefix'); 69 | $roles = []; 70 | 71 | foreach ($this->getAttribute('server_request')->getAttribute('oauth_scopes', []) as $scope) { 72 | $roles[] = strtoupper(trim($prefix . $scope)); 73 | } 74 | 75 | return $roles; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Security/Authentication/Token/OAuth2TokenFactory.php: -------------------------------------------------------------------------------- 1 | rolePrefix = $rolePrefix; 20 | } 21 | 22 | public function createOAuth2Token(ServerRequestInterface $serverRequest, ?UserInterface $user, string $providerKey): OAuth2Token 23 | { 24 | return new OAuth2Token($serverRequest, $user, $this->rolePrefix, $providerKey); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Security/EntryPoint/OAuth2EntryPoint.php: -------------------------------------------------------------------------------- 1 | getStatusCode(), $exception->getHeaders()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Security/Exception/InsufficientScopesException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class InsufficientScopesException extends AuthenticationException 14 | { 15 | public static function create(TokenInterface $token): self 16 | { 17 | $exception = new self('The token has insufficient scopes.', 403); 18 | $exception->setToken($token); 19 | 20 | return $exception; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Security/Exception/Oauth2AuthenticationFailedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Oauth2AuthenticationFailedException extends AuthenticationException 13 | { 14 | public static function create(string $message): self 15 | { 16 | return new self($message, 401); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Security/Firewall/OAuth2Listener.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 53 | $this->authenticationManager = $authenticationManager; 54 | $this->httpMessageFactory = $httpMessageFactory; 55 | $this->oauth2TokenFactory = $oauth2TokenFactory; 56 | $this->providerKey = $providerKey; 57 | } 58 | 59 | public function __invoke(RequestEvent $event) 60 | { 61 | $request = $this->httpMessageFactory->createRequest($event->getRequest()); 62 | 63 | if (!$request->hasHeader('Authorization')) { 64 | return; 65 | } 66 | 67 | try { 68 | /** @var OAuth2Token $authenticatedToken */ 69 | $authenticatedToken = $this->authenticationManager->authenticate($this->oauth2TokenFactory->createOAuth2Token($request, null, $this->providerKey)); 70 | } catch (AuthenticationException $e) { 71 | throw new Oauth2AuthenticationFailedException($e->getMessage(), 401, $e); 72 | } 73 | 74 | if (!$this->isAccessToRouteGranted($event->getRequest(), $authenticatedToken)) { 75 | throw InsufficientScopesException::create($authenticatedToken); 76 | } 77 | 78 | $this->tokenStorage->setToken($authenticatedToken); 79 | } 80 | 81 | private function isAccessToRouteGranted(Request $request, OAuth2Token $token): bool 82 | { 83 | $routeScopes = $request->attributes->get('oauth2_scopes', []); 84 | 85 | if (empty($routeScopes)) { 86 | return true; 87 | } 88 | 89 | $tokenScopes = $token 90 | ->getAttribute('server_request') 91 | ->getAttribute('oauth_scopes'); 92 | 93 | /* 94 | * If the end result is empty that means that all route 95 | * scopes are available inside the issued token scopes. 96 | */ 97 | return empty( 98 | array_diff( 99 | $routeScopes, 100 | $tokenScopes 101 | ) 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Security/Guard/Authenticator/OAuth2Authenticator.php: -------------------------------------------------------------------------------- 1 | 25 | * @author Antonio J. García Lagar 26 | */ 27 | final class OAuth2Authenticator implements AuthenticatorInterface 28 | { 29 | private $httpMessageFactory; 30 | private $resourceServer; 31 | private $oauth2TokenFactory; 32 | private $psr7Request; 33 | 34 | public function __construct(HttpMessageFactoryInterface $httpMessageFactory, ResourceServer $resourceServer, OAuth2TokenFactory $oauth2TokenFactory) 35 | { 36 | $this->httpMessageFactory = $httpMessageFactory; 37 | $this->resourceServer = $resourceServer; 38 | $this->oauth2TokenFactory = $oauth2TokenFactory; 39 | } 40 | 41 | public function start(Request $request, ?AuthenticationException $authException = null): Response 42 | { 43 | $exception = new UnauthorizedHttpException('Bearer'); 44 | 45 | return new Response('', $exception->getStatusCode(), $exception->getHeaders()); 46 | } 47 | 48 | public function supports(Request $request): bool 49 | { 50 | return 0 === strpos($request->headers->get('Authorization', ''), 'Bearer '); 51 | } 52 | 53 | public function getCredentials(Request $request) 54 | { 55 | $psr7Request = $this->httpMessageFactory->createRequest($request); 56 | 57 | try { 58 | $this->psr7Request = $this->resourceServer->validateAuthenticatedRequest($psr7Request); 59 | } catch (OAuthServerException $e) { 60 | throw new AuthenticationException('The resource server rejected the request.', 0, $e); 61 | } 62 | 63 | return $this->psr7Request->getAttribute('oauth_user_id'); 64 | } 65 | 66 | public function getUser($userIdentifier, UserProviderInterface $userProvider): UserInterface 67 | { 68 | return '' === $userIdentifier ? new NullUser() : $userProvider->loadUserByUsername($userIdentifier); 69 | } 70 | 71 | public function checkCredentials($token, UserInterface $user): bool 72 | { 73 | return true; 74 | } 75 | 76 | public function createAuthenticatedToken(UserInterface $user, $providerKey): OAuth2Token 77 | { 78 | $tokenUser = $user instanceof NullUser ? null : $user; 79 | 80 | $oauth2Token = $this->oauth2TokenFactory->createOAuth2Token($this->psr7Request, $tokenUser, $providerKey); 81 | 82 | if (!$this->isAccessToRouteGranted($oauth2Token)) { 83 | throw InsufficientScopesException::create($oauth2Token); 84 | } 85 | 86 | $oauth2Token->setAuthenticated(true); 87 | 88 | return $oauth2Token; 89 | } 90 | 91 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response 92 | { 93 | $this->psr7Request = null; 94 | 95 | throw $exception; 96 | } 97 | 98 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response 99 | { 100 | return $this->psr7Request = null; 101 | } 102 | 103 | public function supportsRememberMe(): bool 104 | { 105 | return false; 106 | } 107 | 108 | private function isAccessToRouteGranted(OAuth2Token $token): bool 109 | { 110 | $routeScopes = $this->psr7Request->getAttribute('oauth2_scopes', []); 111 | 112 | if ([] === $routeScopes) { 113 | return true; 114 | } 115 | 116 | $tokenScopes = $token 117 | ->getAttribute('server_request') 118 | ->getAttribute('oauth_scopes'); 119 | 120 | /* 121 | * If the end result is empty that means that all route 122 | * scopes are available inside the issued token scopes. 123 | */ 124 | return [] === array_diff($routeScopes, $tokenScopes); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Security/User/NullUser.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class NullUser implements UserInterface 13 | { 14 | public function getUsername(): string 15 | { 16 | return ''; 17 | } 18 | 19 | public function getPassword(): string 20 | { 21 | return ''; 22 | } 23 | 24 | public function getSalt(): ?string 25 | { 26 | return null; 27 | } 28 | 29 | public function getRoles(): array 30 | { 31 | return []; 32 | } 33 | 34 | public function eraseCredentials(): void 35 | { 36 | return; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Service/ClientFinderInterface.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 25 | } 26 | 27 | public function revokeCredentialsForUser(UserInterface $user): void 28 | { 29 | $userIdentifier = $user->getUsername(); 30 | 31 | $this->entityManager->createQueryBuilder() 32 | ->update(AccessToken::class, 'at') 33 | ->set('at.revoked', ':revoked') 34 | ->setParameter('revoked', true) 35 | ->where('at.userIdentifier = :userIdentifier') 36 | ->setParameter('userIdentifier', $userIdentifier) 37 | ->getQuery() 38 | ->execute(); 39 | 40 | $queryBuilder = $this->entityManager->createQueryBuilder(); 41 | $queryBuilder 42 | ->update(RefreshToken::class, 'rt') 43 | ->set('rt.revoked', ':revoked') 44 | ->setParameter('revoked', true) 45 | ->where($queryBuilder->expr()->in( 46 | 'rt.accessToken', 47 | $this->entityManager->createQueryBuilder() 48 | ->select('at.identifier') 49 | ->from(AccessToken::class, 'at') 50 | ->where('at.userIdentifier = :userIdentifier') 51 | ->getDQL() 52 | )) 53 | ->setParameter('userIdentifier', $userIdentifier) 54 | ->getQuery() 55 | ->execute(); 56 | 57 | $this->entityManager->createQueryBuilder() 58 | ->update(AuthorizationCode::class, 'ac') 59 | ->set('ac.revoked', ':revoked') 60 | ->setParameter('revoked', true) 61 | ->where('ac.userIdentifier = :userIdentifier') 62 | ->setParameter('userIdentifier', $userIdentifier) 63 | ->getQuery() 64 | ->execute(); 65 | } 66 | 67 | public function revokeCredentialsForClient(Client $client): void 68 | { 69 | $doctrineClient = $this->entityManager 70 | ->getRepository(Client::class) 71 | ->findOneBy(['identifier' => $client->getIdentifier()]); 72 | 73 | $this->entityManager->createQueryBuilder() 74 | ->update(AccessToken::class, 'at') 75 | ->set('at.revoked', ':revoked') 76 | ->setParameter('revoked', true) 77 | ->where('at.client = :client') 78 | ->setParameter('client', $doctrineClient) 79 | ->getQuery() 80 | ->execute(); 81 | 82 | $queryBuilder = $this->entityManager->createQueryBuilder(); 83 | $queryBuilder->update(RefreshToken::class, 'rt') 84 | ->set('rt.revoked', ':revoked') 85 | ->setParameter('revoked', true) 86 | ->where($queryBuilder->expr()->in( 87 | 'rt.accessToken', 88 | $this->entityManager->createQueryBuilder() 89 | ->select('at.identifier') 90 | ->from(AccessToken::class, 'at') 91 | ->where('at.client = :client') 92 | ->getDQL() 93 | )) 94 | ->setParameter('client', $doctrineClient) 95 | ->getQuery() 96 | ->execute(); 97 | 98 | $this->entityManager->createQueryBuilder() 99 | ->update(AuthorizationCode::class, 'ac') 100 | ->set('ac.revoked', ':revoked') 101 | ->setParameter('revoked', true) 102 | ->where('ac.client = :client') 103 | ->setParameter('client', $doctrineClient) 104 | ->getQuery() 105 | ->execute(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Service/CredentialsRevokerInterface.php: -------------------------------------------------------------------------------- 1 | configureDoctrineExtension($container); 24 | $this->configureSecurityExtension($container); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getContainerExtension() 31 | { 32 | return new TrikoderOAuth2Extension(); 33 | } 34 | 35 | private function configureSecurityExtension(ContainerBuilder $container): void 36 | { 37 | /** @var SecurityExtension $extension */ 38 | $extension = $container->getExtension('security'); 39 | $extension->addSecurityListenerFactory(new OAuth2Factory()); 40 | } 41 | 42 | private function configureDoctrineExtension(ContainerBuilder $container): void 43 | { 44 | $container->addCompilerPass( 45 | DoctrineOrmMappingsPass::createXmlMappingDriver( 46 | [ 47 | realpath(__DIR__ . '/Resources/config/doctrine/model') => 'Trikoder\Bundle\OAuth2Bundle\Model', 48 | ], 49 | [ 50 | 'trikoder.oauth2.persistence.doctrine.manager', 51 | ], 52 | 'trikoder.oauth2.persistence.doctrine.enabled', 53 | [ 54 | 'TrikoderOAuth2Bundle' => 'Trikoder\Bundle\OAuth2Bundle\Model', 55 | ] 56 | ) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade 2 | Here you will find upgrade steps between releases. 3 | 4 | ## From 3.1.1 to 3.2.0 5 | 6 | The bundle makes modifications to the existing schema. You will need to run the Doctrine schema update process to sync the changes: 7 | 8 | ```sh 9 | bin/console doctrine:schema:update 10 | ``` 11 | 12 | The schema changes include: 13 | 14 | * Added on delete `CASCADE` on authorization code entity client association 15 | 16 | 17 | ## From 3.1.0 to 3.1.1 18 | 19 | The bundle makes modifications to the existing schema. You will need to run the Doctrine schema update process to sync the changes: 20 | 21 | ```sh 22 | bin/console doctrine:schema:update 23 | ``` 24 | 25 | The schema changes include: 26 | 27 | * Removed the `userIdentifier` index from `oauth2_access_token` and `oauth2_authorization_code` tables 28 | 29 | ## ~~From 3.0 to 3.1.0~~ 30 | 31 | > **NOTE:** This is now obsolete due to issue [#196](https://github.com/trikoder/oauth2-bundle/issues/196). You can safely ignore it. 32 | 33 | ### SQL schema changes 34 | 35 | The bundle makes modifications to the existing schema. You will need to run the Doctrine schema update process to sync the changes: 36 | 37 | ```sh 38 | bin/console doctrine:schema:update 39 | ``` 40 | 41 | The schema changes include: 42 | 43 | * New `userIdentifier` index on the `oauth2_access_token` and `oauth2_authorization_code` tables 44 | 45 | ## From 2.x to 3.0 46 | 47 | ### Console command changes 48 | 49 | #### `trikoder:oauth2:clear-expired-tokens` 50 | 51 | The following options have been renamed: 52 | 53 | * `access-tokens-only` has been renamed to `access-tokens` 54 | * `refresh-tokens-only` has been renamed to `refresh-tokens` 55 | 56 | ### SQL schema changes 57 | 58 | The bundle makes modifications to the existing schema. You will need to run the Doctrine schema update process to sync the changes: 59 | 60 | ```sh 61 | bin/console doctrine:schema:update 62 | ``` 63 | 64 | The schema changes include: 65 | 66 | * New `allow_plain_text_pkce` field on the `oauth2_client` table 67 | * `secret` field on the `oauth2_client` table is now nullable 68 | 69 | ### Interface changes 70 | 71 | The following interfaces have been changed: 72 | 73 | #### `Trikoder\Bundle\OAuth2Bundle\Manager\AuthorizationCodeManagerInterface` 74 | 75 | - [Added the clearExpired() method](https://github.com/trikoder/oauth2-bundle/blob/v3.0.0/Manager/AuthorizationCodeManagerInterface.php#L15) 76 | 77 | ### Method signature changes 78 | 79 | The following method signatures have been changed: 80 | 81 | #### `Trikoder\Bundle\OAuth2Bundle\Model\Client` 82 | 83 | - [Return type for getSecret() is now nullable](https://github.com/trikoder/oauth2-bundle/blob/v3.0.0/Model/Client.php#L60) 84 | 85 | --- 86 | 87 | > **NOTE:** The underlying [league/oauth2-server](https://github.com/thephpleague/oauth2-server) library has been upgraded from version `7.x` to `8.x`. Please check your code if you are directly implementing their interfaces or extending existing non-final classes. 88 | 89 | ## From 1.x to 2.x 90 | 91 | ### PSR-7/17 HTTP transport implementation 92 | 93 | The bundle removed a direct dependency on the [zendframework/zend-diactoros](https://github.com/zendframework/zend-diactoros) package. You now need to explicitly install a PSR 7/17 implementation. We recommand that you use [nyholm/psr7](https://github.com/Nyholm/psr7). Check out this [document](https://github.com/trikoder/oauth2-bundle/blob/v2.0.0/docs/psr-implementation-switching.md) if you wish to use a different implementation. 94 | 95 | ### Scope resolving changes 96 | 97 | Previously [documented](https://github.com/trikoder/oauth2-bundle/blob/v1.1.0/docs/controlling-token-scopes.md) client scope inheriting and restricting is now the new default behavior. You can safely remove the listener from your project. 98 | 99 | ### SQL schema changes 100 | 101 | The bundle adds new tables and constraints to the existing schema. You will need to run the Doctrine schema update process to sync the changes: 102 | 103 | ```sh 104 | bin/console doctrine:schema:update 105 | ``` 106 | 107 | The schema changes include: 108 | 109 | * New `oauth2_authorization_code` table for storing authorization codes 110 | * `access_token` field on the `oauth2_refresh_token` table is now nullable 111 | 112 | ### Interface changes 113 | 114 | The following interfaces have been changed: 115 | 116 | #### `Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface` 117 | 118 | - [Added the remove() method](https://github.com/trikoder/oauth2-bundle/blob/v2.0.0/Manager/ClientManagerInterface.php#L15) 119 | - [Added the list() method](https://github.com/trikoder/oauth2-bundle/blob/v2.0.0/Manager/ClientManagerInterface.php#L20) 120 | 121 | #### `Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface` 122 | 123 | - [Added the clearExpired() method](https://github.com/trikoder/oauth2-bundle/blob/v2.0.0/Manager/AccessTokenManagerInterface.php#L15) 124 | 125 | #### `Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface` 126 | 127 | - [Added the clearExpired() method](https://github.com/trikoder/oauth2-bundle/blob/v2.0.0/Manager/RefreshTokenManagerInterface.php#L15) 128 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trikoder/oauth2-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony bundle which provides OAuth 2.0 authorization/resource server capabilities.", 5 | "keywords": ["oauth2"], 6 | "homepage": "https://www.trikoder.net/", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Antonio Pauletich", 11 | "email": "antonio.pauletich@trikoder.net" 12 | }, 13 | { 14 | "name": "Berislav Balogović", 15 | "email": "berislav.balogovic@trikoder.net" 16 | }, 17 | { 18 | "name": "Petar Obradović", 19 | "email": "petar.obradovic@trikoder.net" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=7.2", 24 | "ajgarlag/psr-http-message-bundle": "^1.1", 25 | "doctrine/doctrine-bundle": "^2.0.8", 26 | "doctrine/orm": "^2.7", 27 | "doctrine/persistence": "^2.4", 28 | "league/oauth2-server": "^8.4.1", 29 | "psr/http-factory": "^1.0", 30 | "symfony/framework-bundle": "^4.4|^5.4", 31 | "symfony/psr-http-message-bridge": "^2.0", 32 | "symfony/security-bundle": "^4.4|^5.4" 33 | }, 34 | "require-dev": { 35 | "ext-timecop": "*", 36 | "ext-xdebug": "*", 37 | "doctrine/dbal": "^2.13.9", 38 | "laminas/laminas-diactoros": "^2.2", 39 | "nyholm/psr7": "^1.2", 40 | "phpunit/phpunit": "^8.5|^9.4", 41 | "symfony/browser-kit": "^4.4|^5.4", 42 | "symfony/phpunit-bridge": "^6.3" 43 | }, 44 | "autoload": { 45 | "psr-4": { "Trikoder\\Bundle\\OAuth2Bundle\\": "" }, 46 | "exclude-from-classmap": [ 47 | "/Tests/" 48 | ] 49 | }, 50 | "scripts": { 51 | "lint": "php-cs-fixer fix", 52 | "test": "phpunit" 53 | }, 54 | "suggest": { 55 | "nelmio/cors-bundle": "For handling CORS requests", 56 | "nyholm/psr7": "For a super lightweight PSR-7/17 implementation", 57 | "defuse/php-encryption": "For better performance when doing encryption" 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "allow-plugins": { 62 | "ocramius/package-versions": true 63 | } 64 | }, 65 | "extra": { 66 | "branch-alias": { 67 | "dev-master": "3.x-dev" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /docs/basic-setup.md: -------------------------------------------------------------------------------- 1 | # Basic setup 2 | 3 | ## Managing clients 4 | 5 | There are several commands available to manage clients. 6 | 7 | ### Add a client 8 | 9 | To add a client you should use the `trikoder:oauth2:create-client` command. 10 | 11 | ```sh 12 | Description: 13 | Creates a new oAuth2 client 14 | 15 | Usage: 16 | trikoder:oauth2:create-client [options] [--] [ []] 17 | 18 | Arguments: 19 | identifier The client identifier 20 | secret The client secret 21 | 22 | Options: 23 | --redirect-uri[=REDIRECT-URI] Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs. (multiple values allowed) 24 | --grant-type[=GRANT-TYPE] Sets allowed grant type for client. Use this option multiple times to set multiple grant types. (multiple values allowed) 25 | --scope[=SCOPE] Sets allowed scope for client. Use this option multiple times to set multiple scopes. (multiple values allowed) 26 | --public Creates a public client (a client which does not have a secret) 27 | --allow-plain-text-pkce Creates a client which is allowed to create an authorization code grant PKCE request with the "plain" code challenge method 28 | ``` 29 | 30 | 31 | ### Update a client 32 | 33 | To update a client you should use the `trikoder:oauth2:update-client` command. 34 | 35 | ```sh 36 | Description: 37 | Updates an oAuth2 client 38 | 39 | Usage: 40 | trikoder:oauth2:update-client [options] [--] 41 | 42 | Arguments: 43 | identifier The client ID 44 | 45 | Options: 46 | --redirect-uri[=REDIRECT-URI] Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs. (multiple values allowed) 47 | --grant-type[=GRANT-TYPE] Sets allowed grant type for client. Use this option multiple times to set multiple grant types. (multiple values allowed) 48 | --scope[=SCOPE] Sets allowed scope for client. Use this option multiple times to set multiple scopes. (multiple values allowed) 49 | --deactivated If provided, it will deactivate the given client. 50 | ``` 51 | 52 | #### Restrict which grant types a client can access 53 | 54 | ```sh 55 | $ bin/console trikoder:oauth2:update-client --grant-type client_credentials --grant-type password foo 56 | ``` 57 | 58 | #### Assign which scopes the client will receive 59 | 60 | 61 | ```sh 62 | $ bin/console trikoder:oauth2:update-client --scope create --scope read foo 63 | ``` 64 | 65 | ### Delete a client 66 | To delete a client you should use the `trikoder:oauth2:delete-client` command. 67 | 68 | ```sh 69 | Description: 70 | Deletes an oAuth2 client 71 | 72 | Usage: 73 | trikoder:oauth2:delete-client 74 | 75 | Arguments: 76 | identifier The client ID 77 | ``` 78 | 79 | ### List clients 80 | To list clients you should use the `trikoder:oauth2:list-clients` command. 81 | 82 | ```sh 83 | Description: 84 | Lists existing oAuth2 clients 85 | 86 | Usage: 87 | trikoder:oauth2:list-clients [options] 88 | 89 | Options: 90 | --columns[=COLUMNS] Determine which columns are shown. Comma separated list. [default: "identifier, secret, scope, redirect uri, grant type"] 91 | --redirect-uri[=REDIRECT-URI] Finds by redirect uri for client. Use this option multiple times to filter by multiple redirect URIs. (multiple values allowed) 92 | --grant-type[=GRANT-TYPE] Finds by allowed grant type for client. Use this option multiple times to filter by multiple grant types. (multiple values allowed) 93 | --scope[=SCOPE] Finds by allowed scope for client. Use this option multiple times to find by multiple scopes. (multiple values allowed)__ 94 | ``` 95 | 96 | ## Configuring the Security layer 97 | 98 | Add two new firewalls in your security configuration: 99 | 100 | ```yaml 101 | security: 102 | firewalls: 103 | api_token: 104 | pattern: ^/api/token$ 105 | security: false 106 | api: 107 | pattern: ^/api 108 | security: true 109 | stateless: true 110 | oauth2: true 111 | ``` 112 | 113 | * The `api_token` firewall will ensure that anyone can access the `/api/token` endpoint in order to be able to retrieve their access tokens. 114 | * The `api` firewall will protect all routes prefixed with `/api` and clients will require a valid access token in order to access them. 115 | 116 | Basically, any firewall which sets the `oauth2` parameter to `true` will make any routes that match the selected pattern go through our OAuth 2.0 security layer. 117 | 118 | ## Configuring the Security layer using Guard 119 | 120 | Alternatively, you can use a guard authenticator to protect your resources: 121 | 122 | ```yaml 123 | security: 124 | firewalls: 125 | api_token: 126 | pattern: ^/api/token$ 127 | security: false 128 | api: 129 | pattern: ^/api 130 | security: true 131 | stateless: true 132 | guard: 133 | authenticators: 134 | - Trikoder\Bundle\OAuth2Bundle\Security\Guard\Authenticator\OAuth2Authenticator 135 | ``` 136 | 137 | > **NOTE:** The order of firewalls is important because Symfony will evaluate them in the specified order. 138 | 139 | ## Restricting routes by scope 140 | 141 | You can define the `oauth2_scopes` parameter on the route you which to restrict the access to. The user will have to authenticate with **all** scopes which you defined: 142 | 143 | ```yaml 144 | oauth2_restricted: 145 | path: /api/restricted 146 | controller: 'App\Controller\FooController::barAction' 147 | defaults: 148 | oauth2_scopes: ['foo', 'bar'] 149 | ``` 150 | 151 | ## Security roles 152 | 153 | Once the user gets past the `oauth2` firewall, they will be granted additional roles based on their granted [token scopes](controlling-token-scopes.md). 154 | By default, the roles are named in the following format: 155 | 156 | ``` 157 | ROLE_OAUTH2_ 158 | ``` 159 | 160 | Here's one of the example uses cases featuring the [@IsGranted](https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html#isgranted) annotation: 161 | 162 | ```php 163 | /** 164 | * @IsGranted("ROLE_OAUTH2_EDIT") 165 | */ 166 | public function indexAction() 167 | { 168 | // ... 169 | } 170 | ``` 171 | 172 | > **NOTE:** You can change the `ROLE_OAUTH2_` prefix via the `role_prefix` configuration option described in [Installation section](../README.md#installation) 173 | 174 | ## Auth 175 | 176 | There are two possible reasons for the authentication server to reject a request: 177 | - Provided token is expired or invalid (HTTP response 401 `Unauthorized`) 178 | - Provided token is valid but scopes are insufficient (HTTP response 403 `Forbidden`) 179 | 180 | ## Clearing expired access, refresh tokens and auth codes 181 | 182 | To clear expired access and refresh tokens and auth codes you can use the `trikoder:oauth2:clear-expired-tokens` command. 183 | 184 | The command removes all tokens whose expiry time is lesser than the current. 185 | 186 | ```sh 187 | Description: 188 | Clears all expired access and/or refresh tokens and/or auth codes 189 | 190 | Usage: 191 | trikoder:oauth2:clear-expired-tokens [options] 192 | 193 | Options: 194 | -a, --access-tokens Clear expired access tokens. 195 | -r, --refresh-tokens Clear expired refresh tokens. 196 | -c, --auth-codes Clear expired auth codes. 197 | ``` 198 | 199 | Not passing any option means that both expired access and refresh tokens as well as expired auth codes 200 | will be cleared. 201 | 202 | ## CORS requests 203 | 204 | For CORS handling, use [NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) 205 | -------------------------------------------------------------------------------- /docs/controlling-token-scopes.md: -------------------------------------------------------------------------------- 1 | # Controlling token scopes 2 | 3 | It's possible to alter issued access token's scopes by subscribing to the `trikoder.oauth2.scope_resolve` event. 4 | 5 | ## Example 6 | 7 | ### Listener 8 | ```php 9 | getScopes(); 20 | 21 | // Make adjustments to the client's requested scopes... 22 | ... 23 | 24 | $event->setScopes(...$requestedScopes); 25 | } 26 | } 27 | ``` 28 | 29 | ### Service configuration 30 | 31 | ```yaml 32 | App\EventListener\ScopeResolveListener: 33 | tags: 34 | - { name: kernel.event_listener, event: trikoder.oauth2.scope_resolve, method: onScopeResolve } 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | Before you start you'll need to setup your IDE, the following is an example in PhpStorm: 4 | 5 | ![PhpStorm XDebug](resources/phpstorm-xdebug.png) 6 | 7 | Then all you need to do is run the following command: 8 | 9 | ```sh 10 | dev/bin/php-debug vendor/bin/phpunit 11 | ``` 12 | 13 | If you wish to use a different server name, you can do this with the 14 | `PHP_IDE_CONFIG` env variable: 15 | 16 | ```sh 17 | PHP_IDE_CONFIG=some_other_name dev/bin/php-debug vendor/bin/phpunit 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/implementing-custom-grant-type.md: -------------------------------------------------------------------------------- 1 | # Implementing custom grant type 2 | 3 | 1. Create a class that implements the `\Trikoder\Bundle\OAuth2Bundle\League\AuthorizationServer\GrantTypeInterface` interface. 4 | 5 | Example: 6 | 7 | ```php 8 | foo = $foo; 31 | } 32 | 33 | public function getIdentifier() 34 | { 35 | return 'fake_grant'; 36 | } 37 | 38 | public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL) 39 | { 40 | return new Response(); 41 | } 42 | 43 | public function getAccessTokenTTL(): ?DateInterval 44 | { 45 | return new DateInterval('PT5H'); 46 | } 47 | } 48 | ``` 49 | 50 | 1. In order to enable the new grant type in the authorization server you must register the service in the container. 51 | The service must be autoconfigured or you have to manually tag it with the `trikoder.oauth2.authorization_server.grant` tag: 52 | 53 | ```yaml 54 | services: 55 | _defaults: 56 | autoconfigure: true 57 | 58 | App\Grant\FakeGrant: ~ 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/password-grant-handling.md: -------------------------------------------------------------------------------- 1 | # Password grant handling 2 | 3 | The `password` grant issues access and refresh tokens that are bound to both a client and a user within your application. As user system implementations can differ greatly on an application basis, the `trikoder.oauth2.user_resolve` was created which allows you to decide which user you want to bind to issuing tokens. 4 | 5 | ## Requirements 6 | 7 | The user model should implement the `Symfony\Component\Security\Core\User\UserInterface` interface. 8 | 9 | ## Example 10 | 11 | ### Listener 12 | 13 | ```php 14 | userProvider = $userProvider; 41 | $this->userPasswordEncoder = $userPasswordEncoder; 42 | } 43 | 44 | /** 45 | * @param UserResolveEvent $event 46 | */ 47 | public function onUserResolve(UserResolveEvent $event): void 48 | { 49 | $user = $this->userProvider->loadUserByUsername($event->getUsername()); 50 | 51 | if (null === $user) { 52 | return; 53 | } 54 | 55 | if (!$this->userPasswordEncoder->isPasswordValid($user, $event->getPassword())) { 56 | return; 57 | } 58 | 59 | $event->setUser($user); 60 | } 61 | } 62 | ``` 63 | 64 | ### Service configuration 65 | 66 | ```yaml 67 | App\EventListener\UserResolveListener: 68 | arguments: 69 | - '@app.repository.user_repository' 70 | - '@security.password_encoder' 71 | tags: 72 | - { name: kernel.event_listener, event: trikoder.oauth2.user_resolve, method: onUserResolve } 73 | ``` 74 | 75 | > **NOTE:** The first dependency in this example should be any service class that implements the `UserProviderInterface` interface. 76 | -------------------------------------------------------------------------------- /docs/psr-implementation-switching.md: -------------------------------------------------------------------------------- 1 | # PSR 7/17 implementation switching 2 | 3 | This bundle requires a PSR 7/17 implementation to operate. We recommend that you use [nyholm/psr7](https://github.com/Nyholm/psr7) as this is the one Symfony [suggests](https://symfony.com/doc/current/components/psr7.html#installation) themselves. 4 | 5 | The recommended implementation requires no extra configuration. Check out the example below to see how to use a different one. 6 | 7 | ## Example 8 | 9 | In this example we'll use the [zendframework/zend-diactoros](https://github.com/zendframework/zend-diactoros) package. 10 | 11 | 1. Require the package via Composer: 12 | 13 | ```sh 14 | composer require zendframework/zend-diactoros 15 | ``` 16 | 17 | 2. Register factory services and alias them to PSR interfaces in your service configuration file: 18 | 19 | ```yaml 20 | services: 21 | # Register services 22 | Zend\Diactoros\ServerRequestFactory: ~ 23 | Zend\Diactoros\StreamFactory: ~ 24 | Zend\Diactoros\UploadedFileFactory: ~ 25 | Zend\Diactoros\ResponseFactory: ~ 26 | 27 | # Setup autowiring aliases 28 | Psr\Http\Message\ServerRequestFactoryInterface: '@Zend\Diactoros\ServerRequestFactory' 29 | Psr\Http\Message\StreamFactoryInterface: '@Zend\Diactoros\StreamFactory' 30 | Psr\Http\Message\UploadedFileFactoryInterface: '@Zend\Diactoros\UploadedFileFactory' 31 | Psr\Http\Message\ResponseFactoryInterface: '@Zend\Diactoros\ResponseFactory' 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/resources/phpstorm-xdebug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trikoder/oauth2-bundle/bbfaada32a1af84a5150df7bbbadab2603cb5db6/docs/resources/phpstorm-xdebug.png --------------------------------------------------------------------------------