├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── generate-oauth2-keys ├── composer.json ├── config └── oauth2.php ├── data ├── .gitignore ├── oauth2.sql └── oauth2_test.sql └── src ├── AuthorizationHandler.php ├── AuthorizationHandlerFactory.php ├── AuthorizationMiddleware.php ├── AuthorizationMiddlewareFactory.php ├── AuthorizationServerFactory.php ├── ConfigProvider.php ├── ConfigTrait.php ├── CryptKeyTrait.php ├── Entity ├── AccessTokenEntity.php ├── AuthCodeEntity.php ├── ClientEntity.php ├── RefreshTokenEntity.php ├── RevokableTrait.php ├── ScopeEntity.php ├── TimestampableTrait.php └── UserEntity.php ├── Exception ├── ExceptionInterface.php ├── InvalidConfigException.php └── RuntimeException.php ├── Grant ├── AuthCodeGrantFactory.php ├── ClientCredentialsGrantFactory.php ├── ImplicitGrantFactory.php ├── PasswordGrantFactory.php └── RefreshTokenGrantFactory.php ├── OAuth2Adapter.php ├── OAuth2AdapterFactory.php ├── Repository └── Pdo │ ├── AbstractRepository.php │ ├── AccessTokenRepository.php │ ├── AccessTokenRepositoryFactory.php │ ├── AuthCodeRepository.php │ ├── AuthCodeRepositoryFactory.php │ ├── ClientRepository.php │ ├── ClientRepositoryFactory.php │ ├── PdoService.php │ ├── PdoServiceFactory.php │ ├── RefreshTokenRepository.php │ ├── RefreshTokenRepositoryFactory.php │ ├── ScopeRepository.php │ ├── ScopeRepositoryFactory.php │ ├── UserRepository.php │ └── UserRepositoryFactory.php ├── RepositoryTrait.php ├── ResourceServerFactory.php ├── TokenEndpointHandler.php └── TokenEndpointHandlerFactory.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.0.1 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 2.0.0 - 2019-12-28 28 | 29 | ### Added 30 | 31 | - Nothing. 32 | 33 | ### Changed 34 | 35 | - [#69](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/69) updates the minimum supported version of league/oauth-server to ^8.0. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - Nothing. 48 | 49 | ## 1.3.0 - 2019-12-28 50 | 51 | ### Added 52 | 53 | - [#62](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/62) adds the ability to configure and add event listeners for the underlying league/oauth2 implementation. See the [event listeners configuration documentation](https://docs.zendframework.com/zend-expressive-authentication-oauth2/intro/#configure-event-listeners) for more information. 54 | 55 | ### Changed 56 | 57 | - Nothing. 58 | 59 | ### Deprecated 60 | 61 | - Nothing. 62 | 63 | ### Removed 64 | 65 | - Nothing. 66 | 67 | ### Fixed 68 | 69 | - Nothing. 70 | 71 | ## 1.2.1 - 2019-12-28 72 | 73 | ### Added 74 | 75 | - Nothing. 76 | 77 | ### Changed 78 | 79 | - [#55](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/55) changes how the `OAuth2Adapter` validates when a client ID is present. Previously, if a client ID was present, but not a user ID, it would attempt to pull a user from the user factory using the client ID, which was incorrect. With this release, it no longer does that. 80 | 81 | ### Deprecated 82 | 83 | - Nothing. 84 | 85 | ### Removed 86 | 87 | - Nothing. 88 | 89 | ### Fixed 90 | 91 | - [#71](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/71) adds a check to `AccessTokenRepository` to verify that a row was returned before checking if a token was revoked, raising an exception if not. 92 | 93 | - [#72](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/72) updates the database schema in provided examples to reflect actual requirements. 94 | 95 | ## 1.2.0 - 2019-09-01 96 | 97 | ### Added 98 | 99 | - [#63](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/63) 100 | registers the `ConfigProvider` with the package. If you are using zend-component-installer 101 | it will be added to your configuration during the installation. 102 | 103 | - [#64](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/64) 104 | adds support for PHP 7.3. 105 | 106 | ### Changed 107 | 108 | - Nothing. 109 | 110 | ### Deprecated 111 | 112 | - Nothing. 113 | 114 | ### Removed 115 | 116 | - Nothing. 117 | 118 | ### Fixed 119 | 120 | - Nothing. 121 | 122 | ## 1.1.0 - 2018-11-19 123 | 124 | ### Added 125 | 126 | - Nothing. 127 | 128 | ### Changed 129 | 130 | - [#58](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/58) 131 | Upgrades the `league/oauth2-server` library to 7.3.0 in order to use it with 132 | [Swoole](https://www.swoole.co.uk/). This is provided by `league/oauth2-server` 133 | thanks to [#960 AuthorizationServer stateless](https://github.com/thephpleague/oauth2-server/pull/960) 134 | 135 | ### Deprecated 136 | 137 | - Nothing. 138 | 139 | ### Removed 140 | 141 | - Nothing. 142 | 143 | ### Fixed 144 | 145 | - Nothing. 146 | 147 | ## 1.0.1 - 2018-10-31 148 | 149 | ### Added 150 | 151 | - Nothing. 152 | 153 | ### Changed 154 | 155 | - Nothing. 156 | 157 | ### Deprecated 158 | 159 | - Nothing. 160 | 161 | ### Removed 162 | 163 | - Nothing. 164 | 165 | ### Fixed 166 | 167 | - [#52](https://github.com/zendframework/zend-expressive-authentication-oauth2/issues/52) 168 | Wrong factory mapped to AuthorizationHandler 169 | - [#54](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/54) 170 | Fixed "WWW-Authenticate" header value format 171 | 172 | ## 1.0.0 - 2018-10-04 173 | 174 | ### Added 175 | 176 | - [#41](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/41) Allows 177 | existing PDO service to be used. This will allow us to reuse existing pdo 178 | services instead of opening up a second connection for oauth. 179 | - [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Adds `TokenEndpointHandler`, 180 | `AuthorizationMiddleware` and `AuthorizationHandler` in the `Zend\Expressive\Authentication\OAuth2` namespace 181 | to [implement an authorization server](https://docs.zendframework.com/zend-expressive-authentication-oauth2/v1/authorization-server/). 182 | - [#50](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/50) Adds 183 | all the OAuth2 identity data generated by [thephpleague/oauth2-server](https://github.com/thephpleague/oauth2-server) 184 | to `UserInterface` PSR-7 attribute. These values are `oauth_user_id`, 185 | `oauth_client_id`, `oauth_access_token_id`, `oauth_scopes`. 186 | 187 | ### Changed 188 | 189 | - [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Splits 190 | `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware` into individual implementations that allow 191 | [OAuth RFC-6749](https://tools.ietf.org/html/rfc6749) compliant authorization server implementations. 192 | 193 | ### Deprecated 194 | 195 | - Nothing. 196 | 197 | ### Removed 198 | 199 | - [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Removes 200 | `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware`. 201 | 202 | ### Fixed 203 | 204 | - [#44](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/44/) Fixes 205 | revocation of access token for PDO repository 206 | - [#45](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/45) Fixes 207 | issue with empty scope being passed throwing exception. 208 | 209 | ## 0.4.3 - 2018-05-09 210 | 211 | ### Added 212 | 213 | - Nothing. 214 | 215 | ### Changed 216 | 217 | - Nothing. 218 | 219 | ### Deprecated 220 | 221 | - Nothing. 222 | 223 | ### Removed 224 | 225 | - Nothing. 226 | 227 | ### Fixed 228 | 229 | - Removes auto-requiring of the encryption key via the configuration unless the default file 230 | actually exists and is readable. As the configuration is processed in every request, this is necessary 231 | to prevent issues when the file does not exist (e.g., if the user has specified an alternate location). 232 | 233 | ## 0.4.2 - 2018-05-09 234 | 235 | ### Added 236 | 237 | - Nothing. 238 | 239 | ### Changed 240 | 241 | - Nothing. 242 | 243 | ### Deprecated 244 | 245 | - Nothing. 246 | 247 | ### Removed 248 | 249 | - Nothing. 250 | 251 | ### Fixed 252 | 253 | - Fixes an issue in the default configuration paths for public, private, and encryption keys, 254 | ensuring they will be based on the current working directory, and not the package directory. 255 | 256 | ## 0.4.1 - 2018-05-09 257 | 258 | ### Added 259 | 260 | - [#30](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/30) 261 | adds the AuthenticationInterface to the config provider so OAuth2 works out of 262 | the box. Can always be overwritten in project configs. 263 | - [#38](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/38) 264 | added the the `/oauth` route configuration in docs. 265 | 266 | ### Changed 267 | 268 | - Nothing. 269 | 270 | ### Deprecated 271 | 272 | - Nothing. 273 | 274 | ### Removed 275 | 276 | - Nothing. 277 | 278 | ### Fixed 279 | 280 | - [#21](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/21) 281 | fixes unknown user will throw an exception. When a user tries to use a 282 | username that doesn't exist in the database an exception is thrown instead of 283 | an invalid_credentials error. 284 | - [#22](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/22) 285 | fixes exception thrown when client secret is missing. When a client id is used 286 | that has no client_secret in the database an exception is thrown instead of an 287 | invalid_client error. 288 | - [#23](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/23) 289 | updates the token insert statements to match schema located in data/oauth2.php 290 | - [#37](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/37) 291 | fixes issue with script to generate keys writes to vendor dir 292 | 293 | 294 | ## 0.4.0 - 2018-03-15 295 | 296 | ### Added 297 | 298 | - [#9](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/9) 299 | adds support for PSR-15. 300 | 301 | - [#13](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/13) 302 | adds `Zend\Expressive\Authentication\OAuth2\Entity\RevokableTrait`, which 303 | provides a way to flag whether or not a token has been revoked, and mimics 304 | traits from the upstream league/oauth2-server implementation. 305 | 306 | - [#13](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/13) 307 | adds `Zend\Expressive\Authentication\OAuth2\Entity\TimestampableTrait`, which 308 | provides methods for setting and retrieving `DateTime` values representing 309 | creation and update timestamps for a token; it mimics traits from the upstream 310 | league/oauth2-server implementation. 311 | 312 | - [#32](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/32) 313 | adds the ability to pull league/oauth2-server grant implementations from the 314 | container, providing factories for each grant type. It also adds the ability 315 | to selectively disable grant types via configuration. 316 | 317 | ### Changed 318 | 319 | - Updates the repository to pin to zend-expressive-authentication `^0.4.0`. 320 | 321 | - [#13](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/13) 322 | updates `Zend\Expressive\Authentication\OAuth2\Entity\AccessTokenEntity` to 323 | use the `RevokableTrait` and `TimestampableTrait`. 324 | 325 | - [#13](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/13) 326 | updates `Zend\Expressive\Authentication\OAuth2\Entity\AuthCodeEntity` to 327 | use the `RevokableTrait`. 328 | 329 | - [#13](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/13) 330 | updates `Zend\Expressive\Authentication\OAuth2\Entity\RefreshTokenEntity` to 331 | use the `RevokableTrait`. 332 | 333 | - [#13](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/13) 334 | updates `Zend\Expressive\Authentication\OAuth2\Entity\ClientEntity` to 335 | use the `RevokableTrait` and `TimestampableTrait`. It also adds methods for 336 | setting and retrieving the client secret, personal access client, and password 337 | client. 338 | 339 | - [#17](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/17) 340 | changes the constructor of each of the `Zend\Expressive\Authentication\OAuth2\OAuth2Adapter` 341 | and `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware` classes to accept 342 | a callable `$responseFactory` instead of a `Psr\Http\Message\ResponseInterface` 343 | response prototype. The `$responseFactory` should produce a 344 | `ResponseInterface` implementation when invoked. 345 | 346 | - [#17](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/17) 347 | updates the `OAuth2AdapterFactory` and `OAuth2MiddlewareFactory` classes to no 348 | longer use `Zend\Expressive\Authentication\ResponsePrototypeTrait`, and 349 | instead always depend on the `Psr\Http\Message\ResponseInterface` service to 350 | correctly return a PHP callable capable of producing a `ResponseInterface` 351 | instance. 352 | 353 | ### Deprecated 354 | 355 | - Nothing. 356 | 357 | ### Removed 358 | 359 | - [#9](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/9) and 360 | [#5](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/5) 361 | remove support for http-interop/http-middleware and 362 | http-interop/http-server-middleware. 363 | 364 | ### Fixed 365 | 366 | - [#18](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/18) 367 | updates the default SQL shipped with the package in `data/oauth2.sql` for 368 | generating OAuth2 tables to ensure it works with MySQL 5.7+; the SQL will 369 | still work with older versions, as well as other relational databases. 370 | 371 | ## 0.3.1 - 2018-02-28 372 | 373 | ### Added 374 | 375 | - Nothing. 376 | 377 | ### Changed 378 | 379 | - Nothing. 380 | 381 | ### Deprecated 382 | 383 | - Nothing. 384 | 385 | ### Removed 386 | 387 | - Nothing. 388 | 389 | ### Fixed 390 | 391 | - [#18](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/18) 392 | updates the default SQL shipped with the package in `data/oauth2.sql` for 393 | generating OAuth2 tables to ensure it works with MySQL 5.7+; the SQL will 394 | still work with older versions, as well as other relational databases. 395 | 396 | ## 0.3.0 - 2018-02-07 397 | 398 | ### Added 399 | 400 | - [#11](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/11) 401 | adds support for zend-expressive-authentication 0.3.0. 402 | 403 | ### Changed 404 | 405 | - Nothing. 406 | 407 | ### Deprecated 408 | 409 | - Nothing. 410 | 411 | ### Removed 412 | 413 | - Nothing. 414 | 415 | ### Fixed 416 | 417 | - Nothing. 418 | 419 | ## 0.2.1 - 2017-12-11 420 | 421 | ### Added 422 | 423 | - [#1](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/1) 424 | adds support for providing configuration for the cryptographic key. This may 425 | be done by providing any of the following via the `authentication.private_key` 426 | configuration: 427 | 428 | - A string representing the key. 429 | - An array with the following key/value pairs: 430 | - `key_or_path` representing either the key or a path on the filesystem to a key. 431 | - `pass_phrase` with the pass phrase to use with the key, if needed. 432 | - `key_permissions_check`, a boolean for indicating whether or not to verify 433 | permissions of the key file before attempting to load it. 434 | 435 | ### Changed 436 | 437 | - Nothing. 438 | 439 | ### Deprecated 440 | 441 | - Nothing. 442 | 443 | ### Removed 444 | 445 | - Nothing. 446 | 447 | ### Fixed 448 | 449 | - Nothing. 450 | 451 | ## 0.2.0 - 2017-11-28 452 | 453 | ### Added 454 | 455 | - Adds support for zend-expressive-authentication 0.2.0. 456 | 457 | ### Changed 458 | 459 | - Nothing. 460 | 461 | ### Deprecated 462 | 463 | - Nothing. 464 | 465 | ### Removed 466 | 467 | - Removes support for zend-expressive-authentication 0.1.0. 468 | 469 | ### Fixed 470 | 471 | - Nothing. 472 | 473 | ## 0.1.0 - 2017-11-20 474 | 475 | Initial release. 476 | 477 | ### Added 478 | 479 | - Everything. 480 | 481 | ### Changed 482 | 483 | - Nothing. 484 | 485 | ### Deprecated 486 | 487 | - Nothing. 488 | 489 | ### Removed 490 | 491 | - Nothing. 492 | 493 | ### Fixed 494 | 495 | - Nothing. 496 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2019, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 server middleware for Expressive and PSR-7 applications 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [mezzio/mezzio-authentication-oauth2](https://github.com/mezzio/mezzio-authentication-oauth2). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-authentication-oauth2.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-authentication-oauth2) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-authentication-oauth2/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-authentication-oauth2?branch=master) 9 | 10 | Zend-expressive-authentication-oauth2 is middleware for [Expressive](https://github.com/zendframework/zend-expressive) 11 | and [PSR-7](http://www.php-fig.org/psr/psr-7/) applications providing an OAuth2 12 | server for authentication. 13 | 14 | This library uses the [league/oauth2-server](https://oauth2.thephpleague.com/) 15 | package for implementing the OAuth2 server. It supports all the following grant 16 | types: 17 | 18 | - client credentials; 19 | - password; 20 | - authorization code; 21 | - implicit; 22 | - refresh token; 23 | 24 | ## Installation 25 | 26 | You can install the *zend-expressive-authentication-oauth2* library with 27 | composer: 28 | 29 | ```bash 30 | $ composer require zendframework/zend-expressive-authentication-oauth2 31 | ``` 32 | 33 | ## Documentation 34 | 35 | Browse the documentation online at https://docs.zendframework.com/zend-expressive-authentication-oauth2/ 36 | 37 | ## Support 38 | 39 | * [Issues](https://github.com/zendframework/zend-expressive-authentication-oauth2/issues/) 40 | * [Chat](https://zendframework-slack.herokuapp.com/) 41 | * [Forum](https://discourse.zendframework.com/) 42 | -------------------------------------------------------------------------------- /bin/generate-oauth2-keys: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $bits = 2048, 68 | 'private_key_type' => OPENSSL_KEYTYPE_RSA, 69 | ]; 70 | 71 | printf('Using %d bits to generate key of type RSA' . "\n\n", $bits); 72 | 73 | // Private key 74 | $res = openssl_pkey_new($config); 75 | 76 | if (!is_resource($res)) { 77 | fwrite(STDERR, 'Failed to create private key.' . PHP_EOL); 78 | fwrite(STDERR, 'Check your openssl extension settings.' . PHP_EOL); 79 | exit(1); 80 | } 81 | 82 | openssl_pkey_export($res, $privateKey); 83 | file_put_contents($filePrivateKey, $privateKey); 84 | printf("Private key stored in:\n%s\n", $filePrivateKey); 85 | 86 | // Public key 87 | $publicKey = openssl_pkey_get_details($res); 88 | file_put_contents($filePublicKey, $publicKey['key']); 89 | printf("Public key stored in:\n%s\n", $filePublicKey); 90 | 91 | // Encryption key 92 | $encKey = base64_encode(random_bytes(32)); 93 | file_put_contents($fileEncryptionKey, sprintf(" getcwd() . '/data/private.key', 15 | 'public_key' => getcwd() . '/data/public.key', 16 | 'access_token_expire' => 'P1D', // 1 day in DateInterval format 17 | 'refresh_token_expire' => 'P1M', // 1 month in DateInterval format 18 | 'auth_code_expire' => 'PT10M', // 10 minutes in DateInterval format 19 | 'pdo' => [ 20 | 'dsn' => '', 21 | 'username' => '', 22 | 'password' => '' 23 | ], 24 | 25 | // Set value to null to disable a grant 26 | 'grants' => [ 27 | \League\OAuth2\Server\Grant\ClientCredentialsGrant::class 28 | => \League\OAuth2\Server\Grant\ClientCredentialsGrant::class, 29 | \League\OAuth2\Server\Grant\PasswordGrant::class 30 | => \League\OAuth2\Server\Grant\PasswordGrant::class, 31 | \League\OAuth2\Server\Grant\AuthCodeGrant::class 32 | => \League\OAuth2\Server\Grant\AuthCodeGrant::class, 33 | \League\OAuth2\Server\Grant\ImplicitGrant::class 34 | => \League\OAuth2\Server\Grant\ImplicitGrant::class, 35 | \League\OAuth2\Server\Grant\RefreshTokenGrant::class 36 | => \League\OAuth2\Server\Grant\RefreshTokenGrant::class 37 | ], 38 | ]; 39 | 40 | // Conditionally include the encryption_key config setting, based on presence of file. 41 | $encryptionKeyFile = getcwd() . '/data/encryption.key'; 42 | if (is_readable($encryptionKeyFile)) { 43 | $config['encryption_key'] = require $encryptionKeyFile; 44 | } 45 | 46 | return $config; 47 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | *.sqlite 3 | -------------------------------------------------------------------------------- /data/oauth2.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Table structure for table `oauth_access_tokens` 3 | -- 4 | 5 | CREATE TABLE `oauth_access_tokens` ( 6 | `id` varchar(100) PRIMARY KEY NOT NULL, 7 | `user_id` int(10) DEFAULT NULL, 8 | `client_id` int(10) NOT NULL, 9 | `name` varchar(255) DEFAULT NULL, 10 | `scopes` text, 11 | `revoked` tinyint(1) NOT NULL DEFAULT '0', 12 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | `updated_at` datetime DEFAULT NULL, 14 | `expires_at` datetime NOT NULL 15 | ); 16 | 17 | CREATE INDEX `IDX_CA42527CA76ED39519EB6921BDA26CCD` ON oauth_access_tokens (`user_id`,`client_id`); 18 | CREATE INDEX `IDX_CA42527CA76ED395` ON oauth_access_tokens (`user_id`); 19 | CREATE INDEX `IDX_CA42527C19EB6921` ON oauth_access_tokens (`client_id`); 20 | 21 | -- 22 | -- Table structure for table `oauth_auth_codes` 23 | -- 24 | 25 | CREATE TABLE `oauth_auth_codes` ( 26 | `id` varchar(100) PRIMARY KEY NOT NULL, 27 | `user_id` int(10) DEFAULT NULL, 28 | `client_id` int(10) NOT NULL, 29 | `scopes` text, 30 | `revoked` tinyint(1) NOT NULL DEFAULT '0', 31 | `expires_at` datetime DEFAULT NULL 32 | ); 33 | 34 | CREATE INDEX `IDX_BB493F83A76ED395` ON oauth_auth_codes (`user_id`); 35 | CREATE INDEX `IDX_BB493F8319EB6921` ON oauth_auth_codes (`client_id`); 36 | 37 | -- 38 | -- Table structure for table `oauth_clients` 39 | -- 40 | 41 | CREATE TABLE `oauth_clients` ( 42 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 43 | `user_id` int(10) DEFAULT NULL, 44 | `name` varchar(100) NOT NULL, 45 | `secret` varchar(100) DEFAULT NULL, 46 | `redirect` varchar(255) DEFAULT NULL, 47 | `personal_access_client` tinyint(1) DEFAULT NULL, 48 | `password_client` tinyint(1) DEFAULT NULL, 49 | `revoked` tinyint(1) DEFAULT NULL, 50 | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, 51 | `updated_at` datetime DEFAULT NULL 52 | ); 53 | 54 | CREATE INDEX `IDX_13CE81015E237E06A76ED395BDA26CCD` ON oauth_clients (`name`,`user_id`); 55 | CREATE INDEX `IDX_13CE8101A76ED395` ON oauth_clients (`user_id`); 56 | 57 | -- 58 | -- Table structure for table `oauth_refresh_tokens` 59 | -- 60 | 61 | CREATE TABLE `oauth_refresh_tokens` ( 62 | `id` varchar(100) PRIMARY KEY NOT NULL, 63 | `access_token_id` varchar(100) NOT NULL, 64 | `revoked` tinyint(1) NOT NULL DEFAULT '0', 65 | `expires_at` datetime NOT NULL 66 | ); 67 | 68 | CREATE INDEX `IDX_5AB6872CCB2688BDA26CCD` ON oauth_refresh_tokens (`access_token_id`); 69 | 70 | -- 71 | -- Table structure for table `oauth_scopes` 72 | -- 73 | 74 | CREATE TABLE `oauth_scopes` ( 75 | `id` varchar(100) PRIMARY KEY NOT NULL 76 | ); 77 | 78 | -- 79 | -- Table structure for table `oauth_users` 80 | -- 81 | 82 | CREATE TABLE `oauth_users` ( 83 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 84 | `username` varchar(320) UNIQUE NOT NULL, 85 | `password` varchar(100) NOT NULL, 86 | `first_name` varchar(80) DEFAULT NULL, 87 | `last_name` varchar(80) DEFAULT NULL 88 | ); 89 | 90 | CREATE INDEX `UNIQ_93804FF8F85E0677` ON oauth_users (`username`); 91 | -------------------------------------------------------------------------------- /data/oauth2_test.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO oauth_clients (name, secret, redirect, personal_access_client, password_client) 2 | VALUES ('client_test', '$2y$10$fFlZTo2Syqa./0JJ2QKV4O/Nfi9cqDMcwHBkN/WMcRLLlaxYUP2CK', '/redirect', 1, 1), 3 | ('client_test2', '$2y$10$fFlZTo2Syqa./0JJ2QKV4O/Nfi9cqDMcwHBkN/WMcRLLlaxYUP2CK', '/redirect', 0, 0); 4 | 5 | INSERT INTO oauth_users (username, password) 6 | VALUES ('user_test', '$2y$10$DW12wQQvr4w7mQ.uSmz37OQkKcIZrRZnpXWoYue7b5v8E/pxvsAru'); 7 | 8 | INSERT INTO oauth_scopes (id) 9 | VALUES ('test'); 10 | -------------------------------------------------------------------------------- /src/AuthorizationHandler.php: -------------------------------------------------------------------------------- 1 | server = $server; 44 | $this->responseFactory = function () use ($responseFactory): ResponseInterface { 45 | return $responseFactory(); 46 | }; 47 | } 48 | 49 | public function handle(ServerRequestInterface $request): ResponseInterface 50 | { 51 | $authRequest = $request->getAttribute(AuthorizationRequest::class); 52 | return $this->server->completeAuthorizationRequest($authRequest, ($this->responseFactory)()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AuthorizationHandlerFactory.php: -------------------------------------------------------------------------------- 1 | get(AuthorizationServer::class), 23 | $container->get(ResponseInterface::class) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AuthorizationMiddleware.php: -------------------------------------------------------------------------------- 1 | server = $server; 50 | $this->responseFactory = function () use ($responseFactory) : ResponseInterface { 51 | return $responseFactory(); 52 | }; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 59 | { 60 | $response = ($this->responseFactory)(); 61 | 62 | try { 63 | $authRequest = $this->server->validateAuthorizationRequest($request); 64 | 65 | // The next handler must take care of providing the 66 | // authenticated user and the approval 67 | $authRequest->setAuthorizationApproved(false); 68 | 69 | return $handler->handle($request->withAttribute(AuthorizationRequest::class, $authRequest)); 70 | } catch (OAuthServerException $exception) { 71 | // The validation throws this exception if the request is not valid 72 | // for example when the client id is invalid 73 | return $exception->generateHttpResponse($response); 74 | } catch (\Exception $exception) { 75 | return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) 76 | ->generateHttpResponse($response); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/AuthorizationMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | get(AuthorizationServer::class), 23 | $container->get(ResponseInterface::class) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AuthorizationServerFactory.php: -------------------------------------------------------------------------------- 1 | getClientRepository($container); 41 | $accessTokenRepository = $this->getAccessTokenRepository($container); 42 | $scopeRepository = $this->getScopeRepository($container); 43 | 44 | $privateKey = $this->getCryptKey($this->getPrivateKey($container), 'authentication.private_key'); 45 | $encryptKey = $this->getEncryptionKey($container); 46 | $grants = $this->getGrantsConfig($container); 47 | 48 | $authServer = new AuthorizationServer( 49 | $clientRepository, 50 | $accessTokenRepository, 51 | $scopeRepository, 52 | $privateKey, 53 | $encryptKey 54 | ); 55 | 56 | $accessTokenInterval = new DateInterval($this->getAccessTokenExpire($container)); 57 | 58 | foreach ($grants as $grant) { 59 | // Config may set this grant to null. Continue on if grant has been disabled 60 | if (empty($grant)) { 61 | continue; 62 | } 63 | 64 | $authServer->enableGrantType( 65 | $container->get($grant), 66 | $accessTokenInterval 67 | ); 68 | } 69 | 70 | // add listeners if configured 71 | $this->addListeners($authServer, $container); 72 | 73 | // add listener providers if configured 74 | $this->addListenerProviders($authServer, $container); 75 | 76 | return $authServer; 77 | } 78 | 79 | /** 80 | * Optionally add event listeners 81 | * 82 | * @param AuthorizationServer $authServer 83 | * @param ContainerInterface $container 84 | */ 85 | private function addListeners( 86 | AuthorizationServer $authServer, 87 | ContainerInterface $container 88 | ): void { 89 | $listeners = $this->getListenersConfig($container); 90 | 91 | foreach ($listeners as $idx => $listenerConfig) { 92 | $event = $listenerConfig[0]; 93 | $listener = $listenerConfig[1]; 94 | $priority = $listenerConfig[2] ?? null; 95 | if (is_string($listener)) { 96 | if (! $container->has($listener)) { 97 | throw new Exception\InvalidConfigException(sprintf( 98 | 'The second element of event_listeners config at ' . 99 | 'index "%s" is a string and therefore expected to ' . 100 | 'be available as a service key in the container. ' . 101 | 'A service named "%s" was not found.', 102 | $idx, 103 | $listener 104 | )); 105 | } 106 | $listener = $container->get($listener); 107 | } 108 | $authServer->getEmitter() 109 | ->addListener($event, $listener, $priority); 110 | } 111 | } 112 | 113 | /** 114 | * Optionally add event listener providers 115 | * 116 | * @param AuthorizationServer $authServer 117 | * @param ContainerInterface $container 118 | */ 119 | private function addListenerProviders( 120 | AuthorizationServer $authServer, 121 | ContainerInterface $container 122 | ): void { 123 | $providers = $this->getListenerProvidersConfig($container); 124 | 125 | foreach ($providers as $idx => $provider) { 126 | if (is_string($provider)) { 127 | if (! $container->has($provider)) { 128 | throw new Exception\InvalidConfigException(sprintf( 129 | 'The event_listener_providers config at ' . 130 | 'index "%s" is a string and therefore expected to ' . 131 | 'be available as a service key in the container. ' . 132 | 'A service named "%s" was not found.', 133 | $idx, 134 | $provider 135 | )); 136 | } 137 | $provider = $container->get($provider); 138 | } 139 | $authServer->getEmitter()->useListenerProvider($provider); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 46 | 'authentication' => include __DIR__ . '/../config/oauth2.php', 47 | 'routes' => $this->getRoutes() 48 | ]; 49 | } 50 | 51 | /** 52 | * Returns the container dependencies 53 | */ 54 | public function getDependencies() : array 55 | { 56 | return [ 57 | 'aliases' => [ 58 | // Choose a different adapter changing the alias value 59 | AccessTokenRepositoryInterface::class => Pdo\AccessTokenRepository::class, 60 | AuthCodeRepositoryInterface::class => Pdo\AuthCodeRepository::class, 61 | ClientRepositoryInterface::class => Pdo\ClientRepository::class, 62 | RefreshTokenRepositoryInterface::class => Pdo\RefreshTokenRepository::class, 63 | ScopeRepositoryInterface::class => Pdo\ScopeRepository::class, 64 | UserRepositoryInterface::class => Pdo\UserRepository::class, 65 | AuthenticationInterface::class => OAuth2Adapter::class 66 | ], 67 | 'factories' => [ 68 | AuthorizationMiddleware::class => AuthorizationMiddlewareFactory::class, 69 | AuthorizationHandler::class => AuthorizationHandlerFactory::class, 70 | TokenEndpointHandler::class => TokenEndpointHandlerFactory::class, 71 | OAuth2Adapter::class => OAuth2AdapterFactory::class, 72 | AuthorizationServer::class => AuthorizationServerFactory::class, 73 | ResourceServer::class => ResourceServerFactory::class, 74 | // Pdo adapter 75 | Pdo\PdoService::class => Pdo\PdoServiceFactory::class, 76 | Pdo\AccessTokenRepository::class => Pdo\AccessTokenRepositoryFactory::class, 77 | Pdo\AuthCodeRepository::class => Pdo\AuthCodeRepositoryFactory::class, 78 | Pdo\ClientRepository::class => Pdo\ClientRepositoryFactory::class, 79 | Pdo\RefreshTokenRepository::class => Pdo\RefreshTokenRepositoryFactory::class, 80 | Pdo\ScopeRepository::class => Pdo\ScopeRepositoryFactory::class, 81 | Pdo\UserRepository::class => Pdo\UserRepositoryFactory::class, 82 | // Default Grants 83 | ClientCredentialsGrant::class => ClientCredentialsGrantFactory::class, 84 | PasswordGrant::class => PasswordGrantFactory::class, 85 | AuthCodeGrant::class => AuthCodeGrantFactory::class, 86 | ImplicitGrant::class => ImplicitGrantFactory::class, 87 | RefreshTokenGrant::class => RefreshTokenGrantFactory::class, 88 | ] 89 | ]; 90 | } 91 | 92 | public function getRoutes() : array 93 | { 94 | return [ 95 | [ 96 | 'name' => 'oauth', 97 | 'path' => '/oauth', 98 | 'middleware' => AuthorizationMiddleware::class, 99 | 'allowed_methods' => ['GET', 'POST'] 100 | ], 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ConfigTrait.php: -------------------------------------------------------------------------------- 1 | get('config')['authentication'] ?? []; 27 | 28 | if (! isset($config['private_key']) || empty($config['private_key'])) { 29 | throw new InvalidConfigException( 30 | 'The private_key value is missing in config authentication' 31 | ); 32 | } 33 | 34 | return $config['private_key']; 35 | } 36 | 37 | protected function getEncryptionKey(ContainerInterface $container) : string 38 | { 39 | $config = $container->get('config')['authentication'] ?? []; 40 | 41 | if (! isset($config['encryption_key']) || empty($config['encryption_key'])) { 42 | throw new InvalidConfigException( 43 | 'The encryption_key value is missing in config authentication' 44 | ); 45 | } 46 | 47 | return $config['encryption_key']; 48 | } 49 | 50 | protected function getAccessTokenExpire(ContainerInterface $container) : string 51 | { 52 | $config = $container->get('config')['authentication'] ?? []; 53 | 54 | if (! isset($config['access_token_expire'])) { 55 | throw new InvalidConfigException( 56 | 'The access_token_expire value is missing in config authentication' 57 | ); 58 | } 59 | 60 | return $config['access_token_expire']; 61 | } 62 | 63 | protected function getRefreshTokenExpire(ContainerInterface $container) : string 64 | { 65 | $config = $container->get('config')['authentication'] ?? []; 66 | 67 | if (! isset($config['refresh_token_expire'])) { 68 | throw new InvalidConfigException( 69 | 'The refresh_token_expire value is missing in config authentication' 70 | ); 71 | } 72 | 73 | return $config['refresh_token_expire']; 74 | } 75 | 76 | protected function getAuthCodeExpire(ContainerInterface $container) : string 77 | { 78 | $config = $container->get('config')['authentication'] ?? []; 79 | 80 | if (! isset($config['auth_code_expire'])) { 81 | throw new Exception\InvalidConfigException( 82 | 'The auth_code_expire value is missing in config authentication' 83 | ); 84 | } 85 | 86 | return $config['auth_code_expire']; 87 | } 88 | 89 | protected function getGrantsConfig(ContainerInterface $container) : array 90 | { 91 | $config = $container->get('config')['authentication'] ?? []; 92 | 93 | if (empty($config['grants'])) { 94 | throw new InvalidConfigException( 95 | 'The grants value is missing in config authentication and must be an array' 96 | ); 97 | } 98 | if (! is_array($config['grants'])) { 99 | throw new InvalidConfigException( 100 | 'The grants must be an array value' 101 | ); 102 | } 103 | 104 | return $config['grants']; 105 | } 106 | 107 | /** 108 | * @param ContainerInterface $container 109 | * 110 | * @return array 111 | */ 112 | protected function getListenersConfig(ContainerInterface $container) : array 113 | { 114 | $config = $container->get('config')['authentication'] ?? []; 115 | 116 | if (empty($config['event_listeners'])) { 117 | return []; 118 | } 119 | if (! is_array($config['event_listeners'])) { 120 | throw new InvalidConfigException( 121 | 'The event_listeners config must be an array value' 122 | ); 123 | } 124 | 125 | return $config['event_listeners']; 126 | } 127 | 128 | /** 129 | * @param ContainerInterface $container 130 | * 131 | * @return array 132 | */ 133 | protected function getListenerProvidersConfig(ContainerInterface $container) : array 134 | { 135 | $config = $container->get('config')['authentication'] ?? []; 136 | 137 | if (empty($config['event_listener_providers'])) { 138 | return []; 139 | } 140 | if (! is_array($config['event_listener_providers'])) { 141 | throw new InvalidConfigException( 142 | 'The event_listener_providers config must be an array value' 143 | ); 144 | } 145 | 146 | return $config['event_listener_providers']; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/CryptKeyTrait.php: -------------------------------------------------------------------------------- 1 | setIdentifier($identifier); 49 | $this->name = $name; 50 | $this->redirectUri = explode(',', $redirectUri); 51 | } 52 | 53 | public function getSecret(): string 54 | { 55 | return $this->secret; 56 | } 57 | 58 | public function setSecret(string $secret): void 59 | { 60 | $this->secret = $secret; 61 | } 62 | 63 | public function hasPersonalAccessClient(): bool 64 | { 65 | return $this->personalAccessClient; 66 | } 67 | 68 | public function setPersonalAccessClient(bool $personalAccessClient): void 69 | { 70 | $this->personalAccessClient = $personalAccessClient; 71 | } 72 | 73 | public function hasPasswordClient(): bool 74 | { 75 | return $this->passwordClient; 76 | } 77 | 78 | public function setPasswordClient(bool $passwordClient): void 79 | { 80 | $this->passwordClient = $passwordClient; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Entity/RefreshTokenEntity.php: -------------------------------------------------------------------------------- 1 | revoked; 26 | } 27 | 28 | /** 29 | * @param bool $revoked 30 | */ 31 | public function setRevoked(bool $revoked): void 32 | { 33 | $this->revoked = $revoked; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Entity/ScopeEntity.php: -------------------------------------------------------------------------------- 1 | getIdentifier(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Entity/TimestampableTrait.php: -------------------------------------------------------------------------------- 1 | createdAt; 34 | } 35 | 36 | public function setCreatedAt(DateTimeInterface $createdAt) : void 37 | { 38 | $this->createdAt = $createdAt; 39 | } 40 | 41 | public function getUpdatedAt() : DateTimeInterface 42 | { 43 | return $this->updatedAt; 44 | } 45 | 46 | public function setUpdatedAt(DateTimeInterface $updatedAt) : void 47 | { 48 | $this->updatedAt = $updatedAt; 49 | } 50 | 51 | /** 52 | * Set createdAt on current date/time if not set, using 53 | * timezone if defined 54 | */ 55 | public function timestampOnCreate() : void 56 | { 57 | if (! $this->createdAt) { 58 | if (method_exists($this, 'getTimezone')) { 59 | $this->createdAt = new DateTimeImmutable('now', new DateTimeZone($this->getTimezone()->getValue())); 60 | } else { 61 | $this->createdAt = new DateTimeImmutable(); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Entity/UserEntity.php: -------------------------------------------------------------------------------- 1 | setIdentifier($identifier); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getAuthCodeRepository($container), 28 | $this->getRefreshTokenRepository($container), 29 | new \DateInterval($this->getAuthCodeExpire($container)) 30 | ); 31 | 32 | $grant->setRefreshTokenTTL( 33 | new \DateInterval($this->getRefreshTokenExpire($container)) 34 | ); 35 | 36 | return $grant; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Grant/ClientCredentialsGrantFactory.php: -------------------------------------------------------------------------------- 1 | getAuthCodeExpire($container)) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Grant/PasswordGrantFactory.php: -------------------------------------------------------------------------------- 1 | getUserRepository($container), 28 | $this->getRefreshTokenRepository($container) 29 | ); 30 | 31 | $grant->setRefreshTokenTTL( 32 | new \DateInterval($this->getRefreshTokenExpire($container)) 33 | ); 34 | 35 | return $grant; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Grant/RefreshTokenGrantFactory.php: -------------------------------------------------------------------------------- 1 | getRefreshTokenRepository($container) 28 | ); 29 | 30 | $grant->setRefreshTokenTTL( 31 | new \DateInterval($this->getRefreshTokenExpire($container)) 32 | ); 33 | 34 | return $grant; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/OAuth2Adapter.php: -------------------------------------------------------------------------------- 1 | resourceServer = $resourceServer; 43 | $this->responseFactory = function () use ($responseFactory) : ResponseInterface { 44 | return $responseFactory(); 45 | }; 46 | $this->userFactory = function ( 47 | string $identity, 48 | array $roles = [], 49 | array $details = [] 50 | ) use ($userFactory) : UserInterface { 51 | return $userFactory($identity, $roles, $details); 52 | }; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function authenticate(ServerRequestInterface $request) : ?UserInterface 59 | { 60 | try { 61 | $result = $this->resourceServer->validateAuthenticatedRequest($request); 62 | $userId = $result->getAttribute('oauth_user_id', null); 63 | $clientId = $result->getAttribute('oauth_client_id', null); 64 | if (isset($userId)) { 65 | return ($this->userFactory)( 66 | $userId, 67 | [], 68 | [ 69 | 'oauth_user_id' => $userId, 70 | 'oauth_client_id' => $clientId, 71 | 'oauth_access_token_id' => $result->getAttribute('oauth_access_token_id', null), 72 | 'oauth_scopes' => $result->getAttribute('oauth_scopes', null) 73 | ] 74 | ); 75 | } 76 | } catch (OAuthServerException $exception) { 77 | return null; 78 | } 79 | return null; 80 | } 81 | 82 | /** 83 | * {@inheritDoc} 84 | */ 85 | public function unauthorizedResponse(ServerRequestInterface $request) : ResponseInterface 86 | { 87 | return ($this->responseFactory)() 88 | ->withHeader( 89 | 'WWW-Authenticate', 90 | 'Bearer realm="OAuth2 token"' 91 | ) 92 | ->withStatus(401); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/OAuth2AdapterFactory.php: -------------------------------------------------------------------------------- 1 | has(ResourceServer::class) 23 | ? $container->get(ResourceServer::class) 24 | : null; 25 | 26 | if (null === $resourceServer) { 27 | throw new Exception\InvalidConfigException( 28 | 'OAuth2 resource server is missing for authentication' 29 | ); 30 | } 31 | 32 | return new OAuth2Adapter( 33 | $resourceServer, 34 | $container->get(ResponseInterface::class), 35 | $container->get(UserInterface::class) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AbstractRepository.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 31 | } 32 | 33 | /** 34 | * Return a string of scopes, separated by space 35 | * from ScopeEntityInterface[] 36 | * 37 | * @param ScopeEntityInterface[] $scopes 38 | * @return string 39 | */ 40 | protected function scopesToString(array $scopes) : string 41 | { 42 | if (empty($scopes)) { 43 | return ''; 44 | } 45 | 46 | return trim(array_reduce($scopes, function ($result, $item) { 47 | return $result . ' ' . $item->getIdentifier(); 48 | })); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AccessTokenRepository.php: -------------------------------------------------------------------------------- 1 | setClient($clientEntity); 33 | foreach ($scopes as $scope) { 34 | $accessToken->addScope($scope); 35 | } 36 | $accessToken->setUserIdentifier($userIdentifier); 37 | return $accessToken; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity) 44 | { 45 | $columns = [ 46 | 'id', 47 | 'user_id', 48 | 'client_id', 49 | 'scopes', 50 | 'revoked', 51 | 'created_at', 52 | 'updated_at', 53 | 'expires_at', 54 | ]; 55 | 56 | $values = [ 57 | ':id', 58 | ':user_id', 59 | ':client_id', 60 | ':scopes', 61 | ':revoked', 62 | 'CURRENT_TIMESTAMP', 63 | 'CURRENT_TIMESTAMP', 64 | ':expires_at', 65 | ]; 66 | 67 | $sth = $this->pdo->prepare(sprintf( 68 | 'INSERT INTO oauth_access_tokens (%s) VALUES (%s)', 69 | implode(', ', $columns), 70 | implode(', ', $values) 71 | )); 72 | 73 | $params = [ 74 | ':id' => $accessTokenEntity->getIdentifier(), 75 | ':user_id' => $accessTokenEntity->getUserIdentifier(), 76 | ':client_id' => $accessTokenEntity->getClient()->getIdentifier(), 77 | ':scopes' => $this->scopesToString($accessTokenEntity->getScopes()), 78 | ':revoked' => 0, 79 | ':expires_at' => date( 80 | 'Y-m-d H:i:s', 81 | $accessTokenEntity->getExpiryDateTime()->getTimestamp() 82 | ), 83 | ]; 84 | 85 | if (false === $sth->execute($params)) { 86 | throw UniqueTokenIdentifierConstraintViolationException::create(); 87 | } 88 | } 89 | 90 | /** 91 | * {@inheritDoc} 92 | */ 93 | public function revokeAccessToken($tokenId) 94 | { 95 | $sth = $this->pdo->prepare( 96 | 'UPDATE oauth_access_tokens SET revoked=:revoked WHERE id = :tokenId' 97 | ); 98 | $sth->bindValue(':revoked', 1); 99 | $sth->bindParam(':tokenId', $tokenId); 100 | 101 | $sth->execute(); 102 | } 103 | 104 | /** 105 | * {@inheritDoc} 106 | */ 107 | public function isAccessTokenRevoked($tokenId) 108 | { 109 | $sth = $this->pdo->prepare( 110 | 'SELECT revoked FROM oauth_access_tokens WHERE id = :tokenId' 111 | ); 112 | $sth->bindParam(':tokenId', $tokenId); 113 | 114 | if (false === $sth->execute()) { 115 | return false; 116 | } 117 | $row = $sth->fetch(); 118 | if (! is_array($row)) { 119 | throw OAuthServerException::invalidRefreshToken(); 120 | } 121 | 122 | return array_key_exists('revoked', $row) ? (bool) $row['revoked'] : false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AccessTokenRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AuthCodeRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 34 | 'INSERT INTO oauth_auth_codes (id, user_id, client_id, scopes, revoked, expires_at) ' . 35 | 'VALUES (:id, :user_id, :client_id, :scopes, :revoked, :expires_at)' 36 | ); 37 | 38 | $sth->bindValue(':id', $authCodeEntity->getIdentifier()); 39 | $sth->bindValue(':user_id', $authCodeEntity->getUserIdentifier()); 40 | $sth->bindValue(':client_id', $authCodeEntity->getClient()->getIdentifier()); 41 | $sth->bindValue(':scopes', $this->scopesToString($authCodeEntity->getScopes())); 42 | $sth->bindValue(':revoked', 0); 43 | $sth->bindValue( 44 | ':expires_at', 45 | date( 46 | 'Y-m-d H:i:s', 47 | $authCodeEntity->getExpiryDateTime()->getTimestamp() 48 | ) 49 | ); 50 | 51 | if (false === $sth->execute()) { 52 | throw UniqueTokenIdentifierConstraintViolationException::create(); 53 | } 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function revokeAuthCode($codeId) 60 | { 61 | $sth = $this->pdo->prepare( 62 | 'UPDATE oauth_auth_codes SET revoked=:revoked WHERE id = :codeId' 63 | ); 64 | $sth->bindValue(':revoked', 1); 65 | $sth->bindParam(':codeId', $codeId); 66 | 67 | $sth->execute(); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function isAuthCodeRevoked($codeId) 74 | { 75 | $sth = $this->pdo->prepare( 76 | 'SELECT revoked FROM oauth_auth_codes WHERE id = :codeId' 77 | ); 78 | $sth->bindParam(':codeId', $codeId); 79 | 80 | if (false === $sth->execute()) { 81 | return false; 82 | } 83 | $row = $sth->fetch(); 84 | 85 | return isset($row['revoked']) ? (bool) $row['revoked'] : false; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Repository/Pdo/AuthCodeRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ClientRepository.php: -------------------------------------------------------------------------------- 1 | getClientData($clientIdentifier); 27 | 28 | if (empty($clientData)) { 29 | return null; 30 | } 31 | 32 | return new ClientEntity( 33 | $clientIdentifier, 34 | $clientData['name'] ?? '', 35 | $clientData['redirect'] ?? '' 36 | ); 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | public function validateClient($clientIdentifier, $clientSecret, $grantType) : bool 43 | { 44 | $clientData = $this->getClientData($clientIdentifier); 45 | 46 | if (empty($clientData)) { 47 | return false; 48 | } 49 | 50 | if (! $this->isGranted($clientData, $grantType)) { 51 | return false; 52 | } 53 | 54 | if (empty($clientData['secret']) || ! password_verify((string) $clientSecret, $clientData['secret'])) { 55 | return false; 56 | } 57 | 58 | return true; 59 | } 60 | 61 | /** 62 | * Check the grantType for the client value, stored in $row 63 | * 64 | * @param array $row 65 | * @param string $grantType 66 | * 67 | * @return bool 68 | */ 69 | protected function isGranted(array $row, string $grantType = null) : bool 70 | { 71 | switch ($grantType) { 72 | case 'authorization_code': 73 | return ! ($row['personal_access_client'] || $row['password_client']); 74 | case 'personal_access': 75 | return (bool) $row['personal_access_client']; 76 | case 'password': 77 | return (bool) $row['password_client']; 78 | default: 79 | return true; 80 | } 81 | } 82 | 83 | private function getClientData(string $clientIdentifier) : ?array 84 | { 85 | $statement = $this->pdo->prepare( 86 | 'SELECT * FROM oauth_clients WHERE name = :clientIdentifier' 87 | ); 88 | $statement->bindParam(':clientIdentifier', $clientIdentifier); 89 | 90 | if ($statement->execute() === false) { 91 | return null; 92 | } 93 | 94 | $row = $statement->fetch(); 95 | 96 | if (empty($row)) { 97 | return null; 98 | } 99 | 100 | return $row; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ClientRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Repository/Pdo/PdoService.php: -------------------------------------------------------------------------------- 1 | has('config') ? $container->get('config') : []; 21 | $config = $config['authentication']['pdo'] ?? null; 22 | if (null === $config) { 23 | throw new Exception\InvalidConfigException( 24 | 'The PDO configuration is missing' 25 | ); 26 | } 27 | 28 | if (is_string($config) && ! $container->has($config)) { 29 | throw new Exception\InvalidConfigException( 30 | 'Invalid service for PDO' 31 | ); 32 | } 33 | 34 | if (is_string($config) && $container->has($config)) { 35 | return $container->get($config); 36 | } 37 | 38 | if (! isset($config['dsn'])) { 39 | throw new Exception\InvalidConfigException( 40 | 'The DSN configuration is missing for PDO' 41 | ); 42 | } 43 | $username = $config['username'] ?? null; 44 | $password = $config['password'] ?? null; 45 | return new PdoService($config['dsn'], $username, $password); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Repository/Pdo/RefreshTokenRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 28 | 'INSERT INTO oauth_refresh_tokens (id, access_token_id, revoked, expires_at) ' . 29 | 'VALUES (:id, :access_token_id, :revoked, :expires_at)' 30 | ); 31 | 32 | $sth->bindValue(':id', $refreshTokenEntity->getIdentifier()); 33 | $sth->bindValue(':access_token_id', $refreshTokenEntity->getAccessToken()->getIdentifier()); 34 | $sth->bindValue(':revoked', 0); 35 | $sth->bindValue( 36 | ':expires_at', 37 | date( 38 | 'Y-m-d H:i:s', 39 | $refreshTokenEntity->getExpiryDateTime()->getTimestamp() 40 | ) 41 | ); 42 | 43 | if (false === $sth->execute()) { 44 | throw UniqueTokenIdentifierConstraintViolationException::create(); 45 | } 46 | } 47 | 48 | public function revokeRefreshToken($tokenId) 49 | { 50 | $sth = $this->pdo->prepare( 51 | 'UPDATE oauth_refresh_tokens SET revoked=:revoked WHERE id = :tokenId' 52 | ); 53 | $sth->bindValue(':revoked', 1); 54 | $sth->bindParam(':tokenId', $tokenId); 55 | 56 | $sth->execute(); 57 | } 58 | 59 | public function isRefreshTokenRevoked($tokenId) 60 | { 61 | $sth = $this->pdo->prepare( 62 | 'SELECT revoked FROM oauth_refresh_tokens WHERE id = :tokenId' 63 | ); 64 | $sth->bindParam(':tokenId', $tokenId); 65 | 66 | if (false === $sth->execute()) { 67 | return false; 68 | } 69 | $row = $sth->fetch(); 70 | 71 | return isset($row['revoked']) ? (bool) $row['revoked'] : false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Repository/Pdo/RefreshTokenRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ScopeRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 22 | 'SELECT id FROM oauth_scopes WHERE id = :identifier' 23 | ); 24 | $sth->bindParam(':identifier', $identifier); 25 | 26 | if (false === $sth->execute()) { 27 | return; 28 | } 29 | 30 | $row = $sth->fetch(); 31 | if (! isset($row['id'])) { 32 | return; 33 | } 34 | 35 | $scope = new ScopeEntity(); 36 | $scope->setIdentifier($row['id']); 37 | return $scope; 38 | } 39 | 40 | public function finalizeScopes( 41 | array $scopes, 42 | $grantType, 43 | ClientEntityInterface $clientEntity, 44 | $userIdentifier = null 45 | ) { 46 | return $scopes; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Repository/Pdo/ScopeRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Repository/Pdo/UserRepository.php: -------------------------------------------------------------------------------- 1 | pdo->prepare( 28 | 'SELECT password FROM oauth_users WHERE username = :username' 29 | ); 30 | $sth->bindParam(':username', $username); 31 | 32 | if (false === $sth->execute()) { 33 | return; 34 | } 35 | 36 | $row = $sth->fetch(); 37 | 38 | if (! empty($row) && password_verify($password, $row['password'])) { 39 | return new UserEntity($username); 40 | } 41 | 42 | return; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Repository/Pdo/UserRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | get(PdoService::class) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/RepositoryTrait.php: -------------------------------------------------------------------------------- 1 | has(UserRepositoryInterface::class)) { 26 | throw new Exception\InvalidConfigException( 27 | 'OAuth2 User Repository is missing' 28 | ); 29 | } 30 | return $container->get(UserRepositoryInterface::class); 31 | } 32 | 33 | protected function getScopeRepository(ContainerInterface $container) : ScopeRepositoryInterface 34 | { 35 | if (! $container->has(ScopeRepositoryInterface::class)) { 36 | throw new Exception\InvalidConfigException( 37 | 'OAuth2 Scope Repository is missing' 38 | ); 39 | } 40 | return $container->get(ScopeRepositoryInterface::class); 41 | } 42 | 43 | protected function getAccessTokenRepository(ContainerInterface $container) : AccessTokenRepositoryInterface 44 | { 45 | if (! $container->has(AccessTokenRepositoryInterface::class)) { 46 | throw new Exception\InvalidConfigException( 47 | 'OAuth2 Access Token Repository is missing' 48 | ); 49 | } 50 | return $container->get(AccessTokenRepositoryInterface::class); 51 | } 52 | 53 | protected function getClientRepository(ContainerInterface $container) : ClientRepositoryInterface 54 | { 55 | if (! $container->has(ClientRepositoryInterface::class)) { 56 | throw new Exception\InvalidConfigException( 57 | 'OAuth2 Client Repository is missing' 58 | ); 59 | } 60 | return $container->get(ClientRepositoryInterface::class); 61 | } 62 | 63 | protected function getRefreshTokenRepository(ContainerInterface $container) : RefreshTokenRepositoryInterface 64 | { 65 | if (! $container->has(RefreshTokenRepositoryInterface::class)) { 66 | throw new Exception\InvalidConfigException( 67 | 'OAuth2 Refresk Token Repository is missing' 68 | ); 69 | } 70 | return $container->get(RefreshTokenRepositoryInterface::class); 71 | } 72 | 73 | protected function getAuthCodeRepository(ContainerInterface $container) : AuthCodeRepositoryInterface 74 | { 75 | if (! $container->has(AuthCodeRepositoryInterface::class)) { 76 | throw new Exception\InvalidConfigException( 77 | 'OAuth2 Refresk Token Repository is missing' 78 | ); 79 | } 80 | return $container->get(AuthCodeRepositoryInterface::class); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ResourceServerFactory.php: -------------------------------------------------------------------------------- 1 | has('config') ? $container->get('config') : []; 24 | $config = $config['authentication'] ?? []; 25 | 26 | if (! isset($config['public_key'])) { 27 | throw new Exception\InvalidConfigException( 28 | 'The public_key value is missing in config authentication' 29 | ); 30 | } 31 | 32 | $publicKey = $this->getCryptKey($config['public_key'], 'authentication.public_key'); 33 | 34 | return new ResourceServer( 35 | $this->getAccessTokenRepository($container), 36 | $publicKey 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/TokenEndpointHandler.php: -------------------------------------------------------------------------------- 1 | server = $server; 48 | $this->responseFactory = $responseFactory; 49 | } 50 | 51 | private function createResponse(): ResponseInterface 52 | { 53 | return ($this->responseFactory)(); 54 | } 55 | 56 | /** 57 | * Request an access token 58 | * 59 | * Used for client credential grant, password grant, and refresh token grant 60 | * 61 | * @see https://oauth2.thephpleague.com/authorization-server/client-credentials-grant/ 62 | * @see https://oauth2.thephpleague.com/authorization-server/resource-owner-password-credentials-grant/ 63 | * @see https://oauth2.thephpleague.com/authorization-server/refresh-token-grant/ 64 | * @see https://tools.ietf.org/html/rfc6749#section-3.2 65 | */ 66 | public function handle(ServerRequestInterface $request): ResponseInterface 67 | { 68 | $response = $this->createResponse(); 69 | 70 | try { 71 | return $this->server->respondToAccessTokenRequest($request, $response); 72 | } catch (OAuthServerException $exception) { 73 | return $exception->generateHttpResponse($response); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/TokenEndpointHandlerFactory.php: -------------------------------------------------------------------------------- 1 | get(AuthorizationServer::class), 23 | $container->get(ResponseInterface::class) 24 | ); 25 | } 26 | } 27 | --------------------------------------------------------------------------------