Repository: thephpleague/oauth2-server Branch: master Commit: 948b278fc873 Files: 143 Total size: 564.1 KB Directory structure: gitextract_nrzlm6b4/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ └── workflows/ │ ├── backwards-compatibility.yml │ ├── coding-standards.yml │ ├── static-analysis.yml │ └── tests.yml ├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── examples/ │ ├── README.md │ ├── composer.json │ ├── public/ │ │ ├── api.php │ │ ├── auth_code.php │ │ ├── client_credentials.php │ │ ├── device_code.php │ │ ├── implicit.php │ │ ├── middleware_use.php │ │ ├── password.php │ │ └── refresh_token.php │ └── src/ │ ├── Entities/ │ │ ├── AccessTokenEntity.php │ │ ├── AuthCodeEntity.php │ │ ├── ClientEntity.php │ │ ├── DeviceCodeEntity.php │ │ ├── RefreshTokenEntity.php │ │ ├── ScopeEntity.php │ │ └── UserEntity.php │ └── Repositories/ │ ├── AccessTokenRepository.php │ ├── AuthCodeRepository.php │ ├── ClientRepository.php │ ├── DeviceCodeRepository.php │ ├── RefreshTokenRepository.php │ ├── ScopeRepository.php │ └── UserRepository.php ├── phpcs.xml.dist ├── phpstan.neon.dist ├── phpunit.xml.dist ├── src/ │ ├── AuthorizationServer.php │ ├── AuthorizationValidators/ │ │ ├── AuthorizationValidatorInterface.php │ │ └── BearerTokenValidator.php │ ├── CodeChallengeVerifiers/ │ │ ├── CodeChallengeVerifierInterface.php │ │ ├── PlainVerifier.php │ │ └── S256Verifier.php │ ├── CryptKey.php │ ├── CryptKeyInterface.php │ ├── CryptTrait.php │ ├── Entities/ │ │ ├── AccessTokenEntityInterface.php │ │ ├── AuthCodeEntityInterface.php │ │ ├── ClientEntityInterface.php │ │ ├── DeviceCodeEntityInterface.php │ │ ├── RefreshTokenEntityInterface.php │ │ ├── ScopeEntityInterface.php │ │ ├── TokenInterface.php │ │ ├── Traits/ │ │ │ ├── AccessTokenTrait.php │ │ │ ├── AuthCodeTrait.php │ │ │ ├── ClientTrait.php │ │ │ ├── DeviceCodeTrait.php │ │ │ ├── EntityTrait.php │ │ │ ├── RefreshTokenTrait.php │ │ │ ├── ScopeTrait.php │ │ │ └── TokenEntityTrait.php │ │ └── UserEntityInterface.php │ ├── EventEmitting/ │ │ ├── AbstractEvent.php │ │ ├── EmitterAwareInterface.php │ │ ├── EmitterAwarePolyfill.php │ │ └── EventEmitter.php │ ├── Exception/ │ │ ├── OAuthServerException.php │ │ └── UniqueTokenIdentifierConstraintViolationException.php │ ├── Grant/ │ │ ├── AbstractAuthorizeGrant.php │ │ ├── AbstractGrant.php │ │ ├── AuthCodeGrant.php │ │ ├── ClientCredentialsGrant.php │ │ ├── DeviceCodeGrant.php │ │ ├── GrantTypeInterface.php │ │ ├── ImplicitGrant.php │ │ ├── PasswordGrant.php │ │ └── RefreshTokenGrant.php │ ├── Middleware/ │ │ ├── AuthorizationServerMiddleware.php │ │ └── ResourceServerMiddleware.php │ ├── RedirectUriValidators/ │ │ ├── RedirectUriValidator.php │ │ └── RedirectUriValidatorInterface.php │ ├── Repositories/ │ │ ├── AccessTokenRepositoryInterface.php │ │ ├── AuthCodeRepositoryInterface.php │ │ ├── ClientRepositoryInterface.php │ │ ├── DeviceCodeRepositoryInterface.php │ │ ├── RefreshTokenRepositoryInterface.php │ │ ├── RepositoryInterface.php │ │ ├── ScopeRepositoryInterface.php │ │ └── UserRepositoryInterface.php │ ├── RequestAccessTokenEvent.php │ ├── RequestEvent.php │ ├── RequestRefreshTokenEvent.php │ ├── RequestTypes/ │ │ ├── AuthorizationRequest.php │ │ └── AuthorizationRequestInterface.php │ ├── ResourceServer.php │ └── ResponseTypes/ │ ├── AbstractResponseType.php │ ├── BearerTokenResponse.php │ ├── DeviceCodeResponse.php │ ├── RedirectResponse.php │ └── ResponseTypeInterface.php └── tests/ ├── AuthorizationServerTest.php ├── AuthorizationValidators/ │ └── BearerTokenValidatorTest.php ├── CodeChallengeVerifiers/ │ ├── PlainVerifierTest.php │ └── S256VerifierTest.php ├── EventEmitting/ │ └── EmitterAwarePolyfillTest.php ├── Exception/ │ └── OAuthServerExceptionTest.php ├── Grant/ │ ├── AbstractGrantTest.php │ ├── AuthCodeGrantTest.php │ ├── ClientCredentialsGrantTest.php │ ├── DeviceCodeGrantTest.php │ ├── ImplicitGrantTest.php │ ├── PasswordGrantTest.php │ └── RefreshTokenGrantTest.php ├── Middleware/ │ ├── AuthorizationServerMiddlewareTest.php │ └── ResourceServerMiddlewareTest.php ├── PHPStan/ │ └── AbstractGrantExtension.php ├── RedirectUriValidators/ │ └── RedirectUriValidatorTest.php ├── ResourceServerTest.php ├── ResponseTypes/ │ ├── BearerResponseTypeTest.php │ ├── BearerTokenResponseWithParams.php │ └── DeviceCodeResponseTypeTest.php ├── Stubs/ │ ├── .gitattributes │ ├── AccessTokenEntity.php │ ├── AuthCodeEntity.php │ ├── ClientEntity.php │ ├── CryptTraitStub.php │ ├── DeviceCodeEntity.php │ ├── GrantType.php │ ├── RefreshTokenEntity.php │ ├── ScopeEntity.php │ ├── StubResponseType.php │ ├── UserEntity.php │ ├── private.key │ ├── private.key.crlf │ └── public.key └── Utils/ ├── CryptKeyTest.php └── CryptTraitTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto /examples export-ignore /tests export-ignore /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore /.scrutinizer.yml export-ignore /.styleci.yml export-ignore /phpstan.neon export-ignore /phpunit.xml.dist export-ignore /CHANGELOG.md export-ignore /CONTRIBUTING.md export-ignore /README.md export-ignore /CODE_OF_CONDUCT.md export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ github: [sephster] ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: composer directory: "/" schedule: interval: daily time: "11:00" open-pull-requests-limit: 10 ignore: - dependency-name: league/event versions: - 3.0.0 ================================================ FILE: .github/workflows/backwards-compatibility.yml ================================================ name: "Backwards compatibility check" on: pull_request: jobs: bc-check: name: "Backwards compatibility check" runs-on: "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v4" with: fetch-depth: 0 - name: Fix git safe.directory in container run: mkdir -p /home/runner/work/_temp/_github_home && printf "[safe]\n\tdirectory = /github/workspace" > /home/runner/work/_temp/_github_home/.gitconfig - name: "Backwards Compatibility Check" uses: docker://nyholm/roave-bc-check-ga with: args: --from=${{ github.event.pull_request.base.sha }} ================================================ FILE: .github/workflows/coding-standards.yml ================================================ name: Coding Standards on: pull_request: push: jobs: coding-standards: name: Coding Standards runs-on: ${{ matrix.operating-system }} strategy: matrix: php-version: - 8.5 operating-system: - ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 with: coverage: none php-version: ${{ matrix.php-version }} ini-values: memory_limit=-1 tools: composer:v2, cs2pr - name: Install Dependencies run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Run Codesniffer run: vendor/bin/phpcs ================================================ FILE: .github/workflows/static-analysis.yml ================================================ name: Static Analysis on: push: pull_request: jobs: static-analysis: name: Static Analysis runs-on: ${{ matrix.operating-system }} strategy: matrix: php-version: [8.2, 8.3, 8.4, 8.5] composer-stability: [prefer-lowest, prefer-stable] operating-system: - ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 with: coverage: none php-version: ${{ matrix.php-version }} ini-values: memory_limit=-1 tools: composer:v2, cs2pr - name: Install Dependencies run: composer update --${{ matrix.composer-stability }} --prefer-dist --no-interaction --no-progress - name: Run Static Analysis run: vendor/bin/phpstan analyse ================================================ FILE: .github/workflows/tests.yml ================================================ name: tests on: push: pull_request: schedule: - cron: "0 0 * * *" jobs: tests: strategy: fail-fast: false matrix: php: [8.2, 8.3, 8.4, 8.5] os: [ubuntu-latest, windows-latest] stability: [prefer-lowest, prefer-stable] runs-on: ${{ matrix.os }} name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, sodium, zip coverage: pcov - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress - name: Install Scrutinizer/Ocular run: composer global require scrutinizer/ocular - name: Execute tests run: vendor/bin/phpunit --coverage-clover=coverage.clover - name: Code coverage if: ${{ github.ref == 'refs/heads/master' && github.repository == 'thephpleague/oauth2-server' && startsWith(matrix.os, 'ubuntu') }} run: ~/.composer/vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover ================================================ FILE: .gitignore ================================================ /vendor /composer.lock phpunit.xml .phpunit.result.cache .idea /examples/vendor examples/public.key examples/private.key build *.orig ================================================ FILE: .scrutinizer.yml ================================================ build: environment: php: version: 8.3.3 nodes: analysis: tests: override: - php-scrutinizer-run filter: excluded_paths: - tests/* - vendor/* checks: php: code_rating: true remove_extra_empty_lines: true remove_php_closing_tag: true remove_trailing_whitespace: true fix_use_statements: remove_unused: true preserve_multiple: false preserve_blanklines: true order_alphabetically: true fix_php_opening_tag: true fix_linefeed: true fix_line_ending: true fix_identation_4spaces: true fix_doc_comments: true tools: external_code_coverage: timeout: 1800 php_code_coverage: false php_code_sniffer: config: standard: PSR2 filter: paths: ['src'] php_loc: enabled: true excluded_dirs: [vendor, tests, examples] php_cpd: enabled: true excluded_dirs: [vendor, tests, examples] ================================================ FILE: .styleci.yml ================================================ preset: psr12 risky: true enabled: - blank_line_before_return - fully_qualified_strict_types - hash_to_slash_comment - include - method_separation - native_function_casing - no_duplicate_semicolons - no_multiline_whitespace_before_semicolons - no_php4_constructor - no_short_bool_cast - no_singleline_whitespace_before_semicolons - no_trailing_comma_in_singleline_array - no_unused_imports - no_whitespace_before_comma_in_array - ordered_imports - phpdoc_align - phpdoc_indent - phpdoc_inline_tag - phpdoc_no_access - phpdoc_no_simplified_null_return - phpdoc_property - phpdoc_scalar - phpdoc_separation - phpdoc_to_comment - phpdoc_trim - phpdoc_type_to_var - phpdoc_types - phpdoc_var_without_name - print_to_echo - short_array_syntax - single_quote - spaces_cast - standardize_not_equal - trailing_comma_in_multiline_array - trim_array_spaces - whitespace_after_comma_in_array ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Changed - User ID is now passed to the finalizeScopes method for the Refresh Grant (PR #1414) ### Removed - Removed support for PHP 8.1 (PR #1500) ## [9.3.0] - released 2025-11-25 ### Added - Added sensitive parameter to avoid sensitive data being included in stack traces (PR #1483) - Support for PHP 8.5 (PR #1492) ### Fixed - Made the Bearer header case insensitive to match the specs correctly (PR #1491) ## [9.2.0] - released 2025-02-15 ### Added - Added a new function to the provided ClientTrait, `supportsGrantType` to allow the auth server to issue the response `unauthorized_client` when applicable (PR #1420) ### Fixed - Fix a bug on setting interval visibility of device authorization grant (PR #1410) - Fix a bug where the new poll date were not persisted when `slow_down` error happens, because the exception is thrown before calling `persistDeviceCode`. (PR #1410) - Fix a bug where `slow_down` error response may have been returned even after the user has completed the auth flow (already approved / denied the request). (PR #1410) - Clients only validated for Refresh, Device Code, and Password grants if the client is confidential (PR #1420) - Emit `RequestAccessTokenEvent` and `RequestRefreshTokenEvent` events instead of the general `RequestEvent` event when an access / refresh token is issued using device authorization grant. (PR #1467) ### Changed - Key permission checks ignored on Windows regardless of userland choice as cannot be run successfully on this OS (PR #1447) ## [9.1.0] - released 2024-11-21 ### Added - Support for PHP 8.4 (PR #1454) ### Fixed - In the Auth Code grant, when requesting an access token with an invalid auth code, we now respond with an invalid_grant error instead of invalid_request (PR #1433) - Fixed spec compliance issue where device access token request was mistakenly expecting to receive scopes in the request (PR #1412) - Refresh tokens pre version 9 might have had user IDs set as ints which meant they were incorrectly rejected. We now cast these values to strings to allow old refresh tokens (PR #1436) ## [9.0.1] - released 2024-10-14 ### Fixed - Auto-generated event emitter is now persisted. Previously, a new emitter was generated every time (PR #1428) - Fixed bug where you could not omit a redirect uri even if one had not been specified during the auth request (PR #1428) - Fixed bug where "state" parameter wasn't present on `invalid_scope` error response and wasn't on fragment part of `access_denied` redirect URI on Implicit grant (PR #1298) - Fixed bug where disabling refresh token revocation via `revokeRefreshTokens(false)` unintentionally disables issuing new refresh token (PR #1449) ## [9.0.0] - released 2024-05-13 ### Added - Device Authorization Grant added (PR #1074) - GrantTypeInterface has a new function, `revokeRefreshTokens()` for enabling or disabling refresh tokens after use (PR #1375) - A CryptKeyInterface to allow developers to change the CryptKey implementation with greater ease (PR #1044) - The authorization server can now finalize scopes when a client uses a refresh token (PR #1094) - An AuthorizationRequestInterface to make it easier to extend the AuthorizationRequest (PR #1110) - Added function `getKeyContents()` to the `CryptKeyInterface` (PR #1375) ### Fixed - Basic authorization is now case insensitive (PR #1403) - If a refresh token has expired, been revoked, cannot be decrypted, or does not belong to the correct client, the server will now issue an `invalid_grant` error and a HTTP 400 response. In previous versions the server incorrectly issued an `invalid_request` and HTTP 401 response (PR #1042) (PR #1082) ### Changed - All interfaces now specify types for all params and return values. Strict typing enforced (PR #1074) - Request parameters are now parsed into strings to use internally in the library (PR #1402) - Authorization Request objects are now created through the factory method, `createAuthorizationRequest()` (PR #1111) - Changed parameters for `finalizeScopes()` to allow a reference to an auth code ID (PR #1112) - AccessTokenEntityInterface now requires the implementation of `toString()` instead of the magic method `__toString()` (PR #1395) ### Removed - Removed message property from OAuthException HTTP response. Now just use error_description as per the OAuth 2 spec (PR #1375) ## [9.0.0-RC1] - released 2024-03-27 ### Added - Device Authorization Grant added (PR #1074) - GrantTypeInterface has a new function, `revokeRefreshTokens()` for enabling or disabling refresh tokens after use (PR #1375) - A CryptKeyInterface to allow developers to change the CryptKey implementation with greater ease (PR #1044) - The authorization server can now finalize scopes when a client uses a refresh token (PR #1094) - An AuthorizationRequestInterface to make it easier to extend the AuthorizationRequest (PR #1110) - Added function `getKeyContents()` to the `CryptKeyInterface` (PR #1375) ### Fixed - If a refresh token has expired, been revoked, cannot be decrypted, or does not belong to the correct client, the server will now issue an `invalid_grant` error and a HTTP 400 response. In previous versions the server incorrectly issued an `invalid_request` and HTTP 401 response (PR #1042) (PR #1082) ### Changed - Authorization Request objects are now created through the factory method, `createAuthorizationRequest()` (PR #1111) - Changed parameters for `finalizeScopes()` to allow a reference to an auth code ID (PR #1112) - AccessTokenEntityInterface now requires the implementation of `toString()` instead of the magic method `__toString()` (PR #1395) ### Removed - Removed message property from OAuthException HTTP response. Now just use error_description as per the OAuth 2 spec (PR #1375) ## [8.5.4] - released 2023-08-25 ### Added - Support for league/uri ^7.0 (PR #1367) ## [8.5.3] - released 2023-07-06 ### Security - If a key string is provided to the CryptKey constructor with an invalid passphrase, the LogicException message generated will expose the given key. The key is no longer leaked via this exception (PR #1353) ## [8.5.2] - released 2023-06-16 ### Changed - Bumped the versions for laminas/diactoros and psr/http-message to support PSR-7 v2.0 (PR #1339) ## [8.5.1] - released 2023-04-04 ### Fixed - Fixed PHP version constraints and lcobucci/clock version constraint to support PHP 8.1 (PR #1336) ## [8.5.0] - released 2023-04-03 ### Added - Support for PHP 8.1 and 8.2 (PR #1333) ### Removed - Support PHP 7.2, 7.3, and 7.4 (PR #1333) ## [8.4.1] - released 2023-03-22 ### Fixed - Fix deprecation notices for PHP 8.x (PR #1329) ## [8.4.0] - released 2023-02-15 ### Added - You can now set a leeway for time drift between servers when validating a JWT (PR #1304) ### Security - Access token requests that contain a code_verifier but are not bound to a code_challenge will be rejected to prevent a PKCE downgrade attack (PR #1326) ## [8.3.6] - released 2022-11-14 ### Fixed - Use LooseValidAt instead of StrictValidAt so that users aren't forced to use claims such as NBF in their JWT tokens (PR #1312) ## [8.3.5] - released 2022-05-12 ### Fixed - Use InMemory::plainText('empty', 'empty') instead of InMemory::plainText('') to avoid [new empty string exception](https://github.com/lcobucci/jwt/pull/833) thrown by lcobucci/jwt (PR #1282) ## [8.3.4] - released 2022-04-07 ### Fixed - Server previously rejected valid uris with custom schemes. Now use league/uri for parsing to accept all valid uris (PR #1274) ## [8.3.3] - released 2021-10-11 ### Security - Removed the use of `LocalFileReference()` in lcobucci/jwt. Function deprecated as per [GHSA-7322-jrq4-x5hf](https://github.com/lcobucci/jwt/security/advisories/GHSA-7322-jrq4-x5hf) (PR #1249) ## [8.3.2] - released 2021-07-27 ### Changed - Conditionally support the `StrictValidAt()` method in lcobucci/jwt so we can use version 4.1.x or greater of the library (PR #1236) - When providing invalid credentials, the library now responds with the error message _The user credentials were incorrect_ (PR #1230) - Keys are always stored in memory now and are not written to a file in the /tmp directory (PR #1180) - The regex for matching the bearer token has been simplified (PR #1238) ## [8.3.1] - released 2021-06-04 ### Fixed - Revert check on clientID. We will no longer require this to be a string (PR #1233) ## [8.3.0] - released 2021-06-03 ### Added - The server will now validate redirect uris according to rfc8252 (PR #1203) - Events emitted now include the refresh token and access token payloads (PR #1211) - Use the `revokeRefreshTokens()` function to decide whether refresh tokens are revoked or not upon use (PR #1189) ### Changed - Keys are now validated using `openssl_pkey_get_private()` and `openssl_pkey_get_public()` instead of regex matching (PR #1215) ### Fixed - The server will now only recognise and handle an authorization header if the value of the header is non-empty. This is to circumvent issues where some common frameworks set this header even if no value is present (PR #1170) - Added type validation for redirect uri, client ID, client secret, scopes, auth code, state, username, and password inputs (PR #1210) - Allow scope "0" to be used. Previously this was removed from a request because it failed an `empty()` check (PR #1181) ## [8.2.4] - released 2020-12-10 ### Fixed - Reverted the enforcement of at least one redirect_uri for a client. This change has instead been moved to version 9 (PR #1169) ## [8.2.3] - released 2020-12-02 ### Added - Re-added support for PHP 7.2 (PR #1165, #1167) ## [8.2.2] - released 2020-11-30 ### Fixed - Fix issue where the private key passphrase isn't correctly passed to JWT library (PR #1164) ## [8.2.1] - released 2020-11-26 ### Fixed - If you have a password on your private key, it is now passed correctly to the JWT configuration object. (PR #1159) ## [8.2.0] - released 2020-11-25 ### Added - Add a `getRedirectUri` function to the `OAuthServerException` class (PR #1123) - Support for PHP 8.0 (PR #1146) ### Removed - Removed support for PHP 7.2 (PR #1146) ### Fixed - Fix typo in parameter hint. `code_challenged` changed to `code_challenge`. Thrown by Auth Code Grant when the code challenge does not match the regex. (PR #1130) - Undefined offset was returned when no client redirect URI was set. Now throw an invalidClient exception if no redirect URI is set against a client (PR #1140) ## [8.1.1] - released 2020-07-01 ### Fixed - If you provide a valid redirect_uri with the auth code grant and an invalid scope, the server will use the given redirect_uri instead of the default client redirect uri (PR #1126) ## [8.1.0] - released 2020-04-29 ### Added - Added support for PHP 7.4 (PR #1075) ### Changed - If an error is encountered when running `preg_match()` to validate an RSA key, the server will now throw a RuntimeException (PR #1047) - Replaced deprecated methods with recommended ones when using `Lcobucci\JWT\Builder` to build a JWT token. (PR #1060) - When storing a key, we no longer touch the file before writing it as this is an unnecessary step (PR #1064) - Prefix native PHP functions in namespaces with backslashes for micro-optimisations (PR #1071) ### Removed - Support for PHP 7.1 (PR #1075) ### Fixed - Clients are now explicitly prevented from using the Client Credentials grant unless they are confidential to conform with the OAuth2 spec (PR #1035) - Abstract method `getIdentifier()` added to AccessTokenTrait. The trait cannot be used without the `getIdentifier()` method being defined (PR #1051) - An exception is now thrown if a refresh token is accidentally sent in place of an authorization code when using the Auth Code Grant (PR #1057) - Can now send access token request without being forced to specify a redirect URI (PR #1096) - In the BearerTokenValidator, if an implementation is using PDO, there is a possibility that a RuntimeException will be thrown when checking if an access token is revoked. This scenario no longer incorrectly issues an exception with a hint mentioning an issue with JSON decoding. (PR #1107) ## [8.0.0] - released 2019-07-13 ### Added - Flag, `requireCodeChallengeForPublicClients`, used to reject public clients that do not provide a code challenge for the Auth Code Grant; use AuthCodeGrant::disableRequireCodeCallengeForPublicClients() to turn off this requirement (PR #938) - Public clients can now use the Auth Code Grant (PR #938) - `isConfidential` getter added to `ClientEntity` to identify type of client (PR #938) - Function `validateClient()` added to validate clients which was previously performed by the `getClientEntity()` function (PR #938) - Add a new function to the AbstractGrant class called `getClientEntityOrFail()`. This is a wrapper around the `getClientEntity()` function that ensures we emit and throw an exception if the repo doesn't return a client entity. (PR #1010) ### Changed - Replace `convertToJWT()` interface with a more generic `__toString()` to improve extensibility; AccessTokenEntityInterface now requires `setPrivateKey(CryptKey $privateKey)` so `__toString()` has everything it needs to work (PR #874) - The `invalidClient()` function accepts a PSR-7 compliant `$serverRequest` argument to avoid accessing the `$_SERVER` global variable and improve testing (PR #899) - `issueAccessToken()` in the Abstract Grant no longer sets access token client, user ID or scopes. These values should already have been set when calling `getNewToken()` (PR #919) - No longer need to enable PKCE with `enableCodeExchangeProof` flag. Any client sending a code challenge will initiate PKCE checks. (PR #938) - Function `getClientEntity()` no longer performs client validation (PR #938) - Password Grant now returns an invalid_grant error instead of invalid_credentials if a user cannot be validated (PR #967) - Use `DateTimeImmutable()` instead of `DateTime()`, `time()` instead of `(new DateTime())->getTimeStamp()`, and `DateTime::getTimeStamp()` instead of `DateTime::format('U')` (PR #963) ### Removed - `enableCodeExchangeProof` flag (PR #938) - Support for PHP 7.0 (PR #1014) - Remove JTI claim from JWT header (PR #1031) ## [7.4.0] - released 2019-05-05 ### Changed - RefreshTokenRepository can now return null, allowing refresh tokens to be optional. (PR #649) ## [7.3.3] - released 2019-03-29 ### Added - Added `error_description` to the error payload to improve standards compliance. The contents of this are copied from the existing `message` value. (PR #1006) ### Deprecated - Error payload will not issue `message` value in the next major release (PR #1006) ## [7.3.2] - released 2018-11-21 ### Fixed - Revert setting keys on response type to be inside `getResponseType()` function instead of AuthorizationServer constructor (PR #969) ## [7.3.1] - released 2018-11-15 ### Fixed - Fix issue with previous release where interface had changed for the AuthorizationServer. Reverted to the previous interface while maintaining functionality changes (PR #970) ## [7.3.0] - released 2018-11-13 ### Changed - Moved the `finalizeScopes()` call from `validateAuthorizationRequest` method to the `completeAuthorizationRequest` method so it is called just before the access token is issued (PR #923) ### Added - Added a ScopeTrait to provide an implementation for jsonSerialize (PR #952) - Ability to nest exceptions (PR #965) ### Fixed - Fix issue where AuthorizationServer is not stateless as ResponseType could store state of a previous request (PR #960) ## [7.2.0] - released 2018-06-23 ### Changed - Added new`validateRedirectUri` method AbstractGrant to remove three instances of code duplication (PR #912) - Allow 640 as a crypt key file permission (PR #917) ### Added - Function `hasRedirect()` added to `OAuthServerException` (PR #703) ### Fixed - Catch and handle `BadMethodCallException` from the `verify()` method of the JWT token in the `validateAuthorization` method (PR #904) ## [4.1.7] - released 2018-06-23 ### Fixed - Ensure `empty()` function call only contains variable to be compatible with PHP 5.4 (PR #918) ## [7.1.1] - released 2018-05-21 ### Fixed - No longer set a WWW-Authenticate header for invalid clients if the client did not send an Authorization header in the original request (PR #902) ## [7.1.0] - released 2018-04-22 ### Changed - Changed hint for unsupportedGrantType exception so it no longer references the grant type parameter which isn't always expected (PR #893) - Upgrade PHPStan checks to level 7 (PR #856) ### Added - Added event emitters for issued access and refresh tokens (PR #860) - Can now use Defuse\Crypto\Key for encryption/decryption of keys which is faster than the Cryto class (PR #812) ### Removed - Remove paragone/random_compat from dependencies ## [7.0.0] - released 2018-02-18 ### Added - Use PHPStan for static analysis of code (PR #848) - Enforce stricter static analysis checks and upgrade library dependencies (PR #852) - Provide PHPStan coverage for tests and update PHPUnit (PR #849) - Get and set methods for OAuth Server Exception payloads. Allow implementer to specify the JSON encode options (PR #719) ### Changed - ClientRepository interface will now accept null for the Grant type to improve extensibility options (PR #607) - Do not issue an error if key file permissions are 400 or 440 (PR #839) - Skip key file creation if the file already exists (PR #845) - Change changelog format and update readme ### Removed - Support for PHP 5.6 - Support for version 5.x and 6.x of the library ### Fixed - PKCE implementation (PR #744) - Set correct redirect URI when validating scopes (PR #840) - S256 code challenege method (PR #842) - Accept RSA key with CRLF line endings (PR #805) ## [6.1.1] - 2017-12-23 - Removed check on empty scopes ## [6.1.0] - 2017-12-23 - Changed the token type issued by the Implicit Grant to be Bearer instead of bearer. (PR #724) - Replaced call to array_key_exists() with the faster isset() on the Implicit Grant. (PR #749) - Allow specification of query delimiter character in the Password Grant (PR #801) - Add Zend Diactoros library dependency to examples (PR #678) - Can set default scope for the authorization endpoint. If no scope is passed during an authorization request, the default scope will be used if set. If not, the server will issue an invalid scope exception (PR #811) - Added validation for redirect URIs on the authorization end point to ensure exactly one redirection URI has been passed (PR #573) ## [6.0.2] - 2017-08-03 - An invalid refresh token that can't be decrypted now returns a HTTP 401 error instead of HTTP 400 (Issue #759) - Removed chmod from CryptKey and add toggle to disable checking (Issue #776) - Fixes invalid code challenge method payload key name (Issue #777) ## [6.0.1] - 2017-07-19 To address feedback from the security release the following change has been made: - If an RSA key cannot be chmod'ed to 600 then it will now throw a E_USER_NOTICE instead of an exception. ## [6.0.0] - 2017-07-01 - Breaking change: The `AuthorizationServer` constructor now expects an encryption key string instead of a public key - Remove support for HHVM - Remove support for PHP 5.5 ## [5.1.4] - 2017-07-01 - Fixed multiple security vulnerabilities as a result of a security audit paid for by the [Mozilla Secure Open Source Fund](https://wiki.mozilla.org/MOSS/Secure_Open_Source). All users of this library are encouraged to update as soon as possible to this version or version 6.0 or greater. - It is recommended on each `AuthorizationServer` instance you set the `setEncryptionKey()`. This will result in stronger encryption being used. If this method is not set messages will be sent to the defined error handling routines (using `error_log`). Please see the examples and documentation for examples. - TravisCI now tests PHP 7.1 (Issue #671) - Fix middleware example fatal error (Issue #682) - Fix typo in the first README sentence (Issue #690) - Corrected DateInterval from 1 min to 1 month (Issue #709) ## [5.1.3] - 2016-10-12 - Fixed WWW-Authenticate header (Issue #669) - Increase the recommended RSA key length from 1024 to 2048 bits (Issue #668) ## [5.1.2] - 2016-09-19 - Fixed `finalizeScopes` call (Issue #650) ## [4.1.6] - 2016-09-13 - Less restrictive on Authorization header check (Issue #652) ## [5.1.1] - 2016-07-26 - Improved test suite (Issue #614) - Updated docblocks (Issue #616) - Replace `array_shift` with `foreach` loop (Issue #621) - Allow easy addition of custom fields to Bearer token response (Issue #624) - Key file auto-generation from string (Issue #625) ## [5.1.0] - 2016-06-28 - Implemented RFC7636 (Issue #574) - Unify middleware exception responses (Issue #578) - Updated examples (Issue #589) - Ensure state is in access denied redirect (Issue #597) - Remove redundant `isExpired()` method from entity interfaces and traits (Issue #600) - Added a check for unique access token constraint violation (Issue #601) - Look at Authorization header directly for HTTP Basic auth checks (Issue #604) - Added catch Runtime exception when parsing JWT string (Issue #605) - Allow `paragonie/random_compat` 2.x (Issue #606) - Added `indigophp/hash-compat` to Composer suggestions and `require-dev` for PHP 5.5 support ## [5.0.3] - 2016-05-04 - Fix hints in PasswordGrant (Issue #560) - Add meaning of `Resource owner` to terminology.md (Issue #561) - Use constant for event name instead of explicit string (Issue #563) - Remove unused request property (Issue #564) - Correct wrong phpdoc (Issue #569) - Fixed typo in exception string (Issue #570) ## [5.0.2] - 2016-04-18 - `state` parameter is now correctly returned after implicit grant authorization - Small code and docblock improvements ## [5.0.1] - 2016-04-18 - Fixes an issue (#550) whereby it was unclear whether or not to validate a client's secret during a request. ## [5.0.0] - 2016-04-17 Version 5 is a complete code rewrite. - Renamed Server class to AuthorizationServer - Added ResourceServer class - Run unit tests again PHP 5.5.9 as it's the minimum supported version - Enable PHPUnit 5.0 support - Improved examples and documentation - Make it clearer that the implicit grant doesn't support refresh tokens - Improved refresh token validation errors - Fixed refresh token expiry date ## [5.0.0-RC2] - 2016-04-10 - Allow multiple client redirect URIs (Issue #511) - Remove unused mac token interface (Issue #503) - Handle RSA key passphrase (Issue #502) - Remove access token repository from response types (Issue #501) - Remove unnecessary methods from entity interfaces (Issue #490) - Ensure incoming JWT hasn't expired (Issue #509) - Fix client identifier passed where user identifier is expected (Issue #498) - Removed built-in entities; added traits to for quick re-use (Issue #504) - Redirect uri is required only if the "redirect_uri" parameter was included in the authorization request (Issue #514) - Removed templating for auth code and implicit grants (Issue #499) ## [5.0.0-RC1] - 2016-03-24 Version 5 is a complete code rewrite. - JWT support - PSR-7 support - Improved exception errors - Replace all occurrences of the term "Storage" with "Repository" - Simplify repositories - Entities conform to interfaces and use traits - Auth code grant updated - Allow support for public clients - Add support for #439 - Client credentials grant updated - Password grant updated - Allow support for public clients - Refresh token grant updated - Implement Implicit grant - Bearer token output type - Remove MAC token output type - Authorization server rewrite - Resource server class moved to PSR-7 middleware - Tests - Much much better documentation ## [4.1.5] - 2016-01-04 - Enable Symfony 3.0 support (#412) ## [4.1.4] - 2015-11-13 - Fix for determining access token in header (Issue #328) - Refresh tokens are now returned for MAC responses (Issue #356) - Added integration list to readme (Issue #341) - Expose parameter passed to exceptions (Issue #345) - Removed duplicate routing setup code (Issue #346) - Docs fix (Issues #347, #360, #380) - Examples fix (Issues #348, #358) - Fix typo in docblock (Issue #352) - Improved timeouts for MAC tokens (Issue #364) - `hash_hmac()` should output raw binary data, not hexits (Issue #370) - Improved regex for matching all Base64 characters (Issue #371) - Fix incorrect signature parameter (Issue #372) - AuthCodeGrant and RefreshTokenGrant don't require client_secret (Issue #377) - Added priority argument to event listener (Issue #388) ## [4.1.3] - 2015-03-22 - Docblock, namespace and inconsistency fixes (Issue #303) - Docblock type fix (Issue #310) - Example bug fix (Issue #300) - Updated league/event to ~2.1 (Issue #311) - Fixed missing session scope (Issue #319) - Updated interface docs (Issue #323) - `.travis.yml` updates ## [4.1.2] - 2015-01-01 - Remove side-effects in hash_equals() implementation (Issue #290) ## [4.1.1] - 2014-12-31 - Changed `symfony/http-foundation` dependency version to `~2.4` so package can be installed in Laravel `4.1.*` ## [4.1.0] - 2014-12-27 - Added MAC token support (Issue #158) - Fixed example init code (Issue #280) - Toggle refresh token rotation (Issue #286) - Docblock fixes ## [4.0.5] - 2014-12-15 - Prevent duplicate session in auth code grant (Issue #282) ## [4.0.4] - 2014-12-03 - Ensure refresh token hasn't expired (Issue #270) ## [4.0.3] - 2014-12-02 - Fix bad type hintings (Issue #267) - Do not forget to set the expire time (Issue #268) ## [4.0.2] - 2014-11-21 - Improved interfaces (Issue #255) - Learnt how to spell delimiter and so `getScopeDelimiter()` and `setScopeDelimiter()` methods have been renamed - Docblock improvements (Issue #254) ## [4.0.1] - 2014-11-09 - Alias the master branch in composer.json (Issue #243) - Numerous PHP CodeSniffer fixes (Issue #244) - .travis.yml update (Issue #245) - The getAccessToken method should return an AccessTokenEntity object instead of a string in ResourceServer.php (#246) ## [4.0.0] - 2014-11-08 - Complete rewrite - Check out the documentation - [http://oauth2.thephpleague.com](http://oauth2.thephpleague.com) ## [3.2.0] - 2014-04-16 - Added the ability to change the algorithm that is used to generate the token strings (Issue #151) ## [3.1.2] - 2014-02-26 - Support Authorization being an environment variable. [See more](http://fortrabbit.com/docs/essentials/quirks-and-constraints#authorization-header) ## [3.1.1] - 2013-12-05 - Normalize headers when `getallheaders()` is available (Issues #108 and #114) ## [3.1.0] - 2013-12-05 - No longer necessary to inject the authorisation server into a grant, the server will inject itself - Added test for 1419ba8cdcf18dd034c8db9f7de86a2594b68605 ## [3.0.1] - 2013-12-02 - Forgot to tell TravisCI from testing PHP 5.3 ## [3.0.0] - 2013-12-02 - Fixed spelling of Implicit grant class (Issue #84) - Travis CI now tests for PHP 5.5 - Fixes for checking headers for resource server (Issues #79 and #) - The word "bearer" now has a capital "B" in JSON output to match OAuth 2.0 spec - All grants no longer remove old sessions by default - All grants now support custom access token TTL (Issue #92) - All methods which didn't before return a value now return `$this` to support method chaining - Removed the build in DB providers - these will be put in their own repos to remove baggage in the main repository - Removed support for PHP 5.3 because this library now uses traits and will use other modern PHP features going forward - Moved some grant related functions into a trait to reduce duplicate code ## [2.1.1] - 2013-06-02 - Added conditional `isValid()` flag to check for Authorization header only (thanks @alexmcroberts) - Fixed semantic meaning of `requireScopeParam()` and `requireStateParam()` by changing their default value to true - Updated some duff docblocks - Corrected array key call in Resource.php (Issue #63) ## [2.1.0] - 2013-05-10 - Moved zetacomponents/database to "suggest" in composer.json. If you rely on this feature you now need to include " zetacomponents/database" into "require" key in your own composer.json. (Issue #51) - New method in Refresh grant called `rotateRefreshTokens()`. Pass in `true` to issue a new refresh token each time an access token is refreshed. This parameter needs to be set to true in order to request reduced scopes with the new access token. (Issue #47) - Rename `key` column in oauth_scopes table to `scope` as `key` is a reserved SQL word. (Issue #45) - The `scope` parameter is no longer required by default as per the RFC. (Issue #43) - You can now set multiple default scopes by passing an array into `setDefaultScope()`. (Issue #42) - The password and client credentials grants now allow for multiple sessions per user. (Issue #32) - Scopes associated to authorization codes are not held in their own table (Issue #44) - Database schema updates. ## [2.0.5] - 2013-05-09 - Fixed `oauth_session_token_scopes` table primary key - Removed `DEFAULT ''` that has slipped into some tables - Fixed docblock for `SessionInterface::associateRefreshToken()` ## [2.0.4] - 2013-05-09 - Renamed primary key in oauth_client_endpoints table - Adding missing column to oauth_session_authcodes ### Security - A refresh token should be bound to a client ID ## [2.0.3] - 2013-05-08 - Fixed a link to code in composer.json ## [2.0.2] - 2013-05-08 - Updated README with wiki guides - Removed `null` as default parameters in some methods in the storage interfaces - Fixed license copyright ## [2.0.0] - 2013-05-08 **If you're upgrading from v1.0.8 there are lots of breaking changes** - Rewrote the session storage interface from scratch so methods are more obvious - Included a PDO driver which implements the storage interfaces so the library is more "get up and go" - Further normalised the database structure so all sessions no longer contain infomation related to authorization grant (which may or may not be enabled) - A session can have multiple associated access tokens - Individual grants can have custom expire times for access tokens - Authorization codes now have a TTL of 10 minutes by default (can be manually set) - Refresh tokens now have a TTL of one week by default (can be manually set) - The client credentials grant will no longer gives out refresh tokens as per the specification ## [1.0.8] - 2013-03-18 - Fixed check for required state parameter - Fixed check that user's credentials are correct in Password grant ## [1.0.7] - 2013-03-04 - Added method `requireStateParam()` - Added method `requireScopeParam()` ## [1.0.6] - 2013-02-22 - Added links to tutorials in the README - Added missing `state` parameter request to the `checkAuthoriseParams()` method. ## [1.0.5] - 2013-02-21 - Fixed the SQL example for SessionInterface::getScopes() ## [1.0.3] - 2013-02-20 - Changed all instances of the "authentication server" to "authorization server" ## [1.0.2] - 2013-02-20 - Fixed MySQL create table order - Fixed version number in composer.json ## [1.0.1] - 2013-02-19 - Updated AuthServer.php to use `self::getParam()` ## 1.0.0 - 2013-02-15 - First major release [Unreleased]: https://github.com/thephpleague/oauth2-server/compare/9.3.0...HEAD [9.3.0]: https://github.com/thephpleague/oauth2-server/compare/9.2.0...9.3.0 [9.2.0]: https://github.com/thephpleague/oauth2-server/compare/9.1.0...9.2.0 [9.1.0]: https://github.com/thephpleague/oauth2-server/compare/9.0.1...9.1.0 [9.0.1]: https://github.com/thephpleague/oauth2-server/compare/9.0.0...9.0.1 [9.0.0]: https://github.com/thephpleague/oauth2-server/compare/9.0.0-RC1...9.0.0 [9.0.0-RC1]: https://github.com/thephpleague/oauth2-server/compare/8.5.4...9.0.0-RC1 [8.5.4]: https://github.com/thephpleague/oauth2-server/compare/8.5.3...8.5.4 [8.5.3]: https://github.com/thephpleague/oauth2-server/compare/8.5.2...8.5.3 [8.5.2]: https://github.com/thephpleague/oauth2-server/compare/8.5.1...8.5.2 [8.5.1]: https://github.com/thephpleague/oauth2-server/compare/8.5.0...8.5.1 [8.5.0]: https://github.com/thephpleague/oauth2-server/compare/8.4.1...8.5.0 [8.4.1]: https://github.com/thephpleague/oauth2-server/compare/8.4.0...8.4.1 [8.4.0]: https://github.com/thephpleague/oauth2-server/compare/8.3.6...8.4.0 [8.3.6]: https://github.com/thephpleague/oauth2-server/compare/8.3.5...8.3.6 [8.3.5]: https://github.com/thephpleague/oauth2-server/compare/8.3.4...8.3.5 [8.3.4]: https://github.com/thephpleague/oauth2-server/compare/8.3.3...8.3.4 [8.3.3]: https://github.com/thephpleague/oauth2-server/compare/8.3.2...8.3.3 [8.3.2]: https://github.com/thephpleague/oauth2-server/compare/8.3.1...8.3.2 [8.3.1]: https://github.com/thephpleague/oauth2-server/compare/8.3.0...8.3.1 [8.3.0]: https://github.com/thephpleague/oauth2-server/compare/8.2.4...8.3.0 [8.2.4]: https://github.com/thephpleague/oauth2-server/compare/8.2.3...8.2.4 [8.2.3]: https://github.com/thephpleague/oauth2-server/compare/8.2.2...8.2.3 [8.2.2]: https://github.com/thephpleague/oauth2-server/compare/8.2.1...8.2.2 [8.2.1]: https://github.com/thephpleague/oauth2-server/compare/8.2.0...8.2.1 [8.2.0]: https://github.com/thephpleague/oauth2-server/compare/8.1.1...8.2.0 [8.1.1]: https://github.com/thephpleague/oauth2-server/compare/8.1.0...8.1.1 [8.1.0]: https://github.com/thephpleague/oauth2-server/compare/8.0.0...8.1.0 [8.0.0]: https://github.com/thephpleague/oauth2-server/compare/7.4.0...8.0.0 [7.4.0]: https://github.com/thephpleague/oauth2-server/compare/7.3.3...7.4.0 [7.3.3]: https://github.com/thephpleague/oauth2-server/compare/7.3.2...7.3.3 [7.3.2]: https://github.com/thephpleague/oauth2-server/compare/7.3.1...7.3.2 [7.3.1]: https://github.com/thephpleague/oauth2-server/compare/7.3.0...7.3.1 [7.3.0]: https://github.com/thephpleague/oauth2-server/compare/7.2.0...7.3.0 [7.2.0]: https://github.com/thephpleague/oauth2-server/compare/7.1.1...7.2.0 [7.1.1]: https://github.com/thephpleague/oauth2-server/compare/7.1.0...7.1.1 [7.1.0]: https://github.com/thephpleague/oauth2-server/compare/7.0.0...7.1.0 [7.0.0]: https://github.com/thephpleague/oauth2-server/compare/6.1.1...7.0.0 [6.1.1]: https://github.com/thephpleague/oauth2-server/compare/6.0.0...6.1.1 [6.1.0]: https://github.com/thephpleague/oauth2-server/compare/6.0.2...6.1.0 [6.0.2]: https://github.com/thephpleague/oauth2-server/compare/6.0.1...6.0.2 [6.0.1]: https://github.com/thephpleague/oauth2-server/compare/6.0.0...6.0.1 [6.0.0]: https://github.com/thephpleague/oauth2-server/compare/5.1.4...6.0.0 [5.1.4]: https://github.com/thephpleague/oauth2-server/compare/5.1.3...5.1.4 [5.1.3]: https://github.com/thephpleague/oauth2-server/compare/5.1.2...5.1.3 [5.1.2]: https://github.com/thephpleague/oauth2-server/compare/5.1.1...5.1.2 [5.1.1]: https://github.com/thephpleague/oauth2-server/compare/5.1.0...5.1.1 [5.1.0]: https://github.com/thephpleague/oauth2-server/compare/5.0.2...5.1.0 [5.0.3]: https://github.com/thephpleague/oauth2-server/compare/5.0.3...5.0.2 [5.0.2]: https://github.com/thephpleague/oauth2-server/compare/5.0.1...5.0.2 [5.0.1]: https://github.com/thephpleague/oauth2-server/compare/5.0.0...5.0.1 [5.0.0]: https://github.com/thephpleague/oauth2-server/compare/5.0.0-RC2...5.0.0 [5.0.0-RC2]: https://github.com/thephpleague/oauth2-server/compare/5.0.0-RC1...5.0.0-RC2 [5.0.0-RC1]: https://github.com/thephpleague/oauth2-server/compare/4.1.5...5.0.0-RC1 [4.1.7]: https://github.com/thephpleague/oauth2-server/compare/4.1.6...4.1.7 [4.1.6]: https://github.com/thephpleague/oauth2-server/compare/4.1.5...4.1.6 [4.1.5]: https://github.com/thephpleague/oauth2-server/compare/4.1.4...4.1.5 [4.1.4]: https://github.com/thephpleague/oauth2-server/compare/4.1.3...4.1.4 [4.1.3]: https://github.com/thephpleague/oauth2-server/compare/4.1.2...4.1.3 [4.1.2]: https://github.com/thephpleague/oauth2-server/compare/4.1.1...4.1.2 [4.1.1]: https://github.com/thephpleague/oauth2-server/compare/4.0.0...4.1.1 [4.1.0]: https://github.com/thephpleague/oauth2-server/compare/4.0.5...4.1.0 [4.0.5]: https://github.com/thephpleague/oauth2-server/compare/4.0.4...4.0.5 [4.0.4]: https://github.com/thephpleague/oauth2-server/compare/4.0.3...4.0.4 [4.0.3]: https://github.com/thephpleague/oauth2-server/compare/4.0.2...4.0.3 [4.0.2]: https://github.com/thephpleague/oauth2-server/compare/4.0.1...4.0.2 [4.0.1]: https://github.com/thephpleague/oauth2-server/compare/4.0.0...4.0.1 [4.0.0]: https://github.com/thephpleague/oauth2-server/compare/3.2.0...4.0.0 [3.2.0]: https://github.com/thephpleague/oauth2-server/compare/3.1.2...3.2.0 [3.1.2]: https://github.com/thephpleague/oauth2-server/compare/3.1.1...3.1.2 [3.1.1]: https://github.com/thephpleague/oauth2-server/compare/3.1.0...3.1.1 [3.1.0]: https://github.com/thephpleague/oauth2-server/compare/3.0.1...3.1.0 [3.0.1]: https://github.com/thephpleague/oauth2-server/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/thephpleague/oauth2-server/compare/2.1.1...3.0.0 [2.1.1]: https://github.com/thephpleague/oauth2-server/compare/2.1.0...2.1.1 [2.1.0]: https://github.com/thephpleague/oauth2-server/compare/2.0.5...2.1.0 [2.0.5]: https://github.com/thephpleague/oauth2-server/compare/2.0.4...2.0.5 [2.0.4]: https://github.com/thephpleague/oauth2-server/compare/2.0.3...2.0.4 [2.0.3]: https://github.com/thephpleague/oauth2-server/compare/2.0.2...2.0.3 [2.0.2]: https://github.com/thephpleague/oauth2-server/compare/2.0.0...2.0.2 [2.0.0]: https://github.com/thephpleague/oauth2-server/compare/1.0.8...2.0.0 [1.0.8]: https://github.com/thephpleague/oauth2-server/compare/1.0.7...1.0.8 [1.0.7]: https://github.com/thephpleague/oauth2-server/compare/1.0.6...1.0.7 [1.0.6]: https://github.com/thephpleague/oauth2-server/compare/1.0.5...1.0.6 [1.0.5]: https://github.com/thephpleague/oauth2-server/compare/1.0.3...1.0.5 [1.0.3]: https://github.com/thephpleague/oauth2-server/compare/1.0.2...1.0.3 [1.0.2]: https://github.com/thephpleague/oauth2-server/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/thephpleague/oauth2-server/compare/1.0.0...1.0.1 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andrew@noexceptions.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org ================================================ FILE: CONTRIBUTING.md ================================================ Thanks for contributing to this project. **Please submit your pull request against the `master` branch only.** Please ensure that you run `phpunit` from the project root after you've made any changes. If you've added something new please create a new unit test, if you've changed something please update any unit tests as appropritate. We're trying to ensure there is **100%** test code coverage (including testing PHP errors and exceptions) so please ensure any new/updated tests cover all of your changes. Thank you, @alexbilbie ================================================ FILE: LICENSE ================================================ MIT License Copyright (C) Alex Bilbie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # PHP OAuth 2.0 Server [![Latest Version](http://img.shields.io/packagist/v/league/oauth2-server.svg?style=flat-square)](https://github.com/thephpleague/oauth2-server/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) [![Build Status](https://github.com/thephpleague/oauth2-server/workflows/tests/badge.svg)](https://github.com/thephpleague/oauth2-server/actions) [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/oauth2-server.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/oauth2-server/code-structure) [![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/oauth2-server.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/oauth2-server) [![Total Downloads](https://img.shields.io/packagist/dt/league/oauth2-server.svg?style=flat-square)](https://packagist.org/packages/league/oauth2-server) `league/oauth2-server` is a standards compliant implementation of an [OAuth 2.0](https://tools.ietf.org/html/rfc6749) authorization server written in PHP which makes working with OAuth 2.0 trivial. You can easily configure an OAuth 2.0 server to protect your API with access tokens, or allow clients to request new access tokens and refresh them. Out of the box it supports the following grants: - Authorization code grant - Client credentials grant - Device authorization grant - Implicit grant - Refresh grant - Resource owner password credentials grant The following RFCs are implemented: - [RFC6749 "OAuth 2.0"](https://tools.ietf.org/html/rfc6749) - [RFC6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage"](https://tools.ietf.org/html/rfc6750) - [RFC7519 "JSON Web Token (JWT)"](https://tools.ietf.org/html/rfc7519) - [RFC7636 "Proof Key for Code Exchange by OAuth Public Clients"](https://tools.ietf.org/html/rfc7636) - [RFC8628 "OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628) This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](https://twitter.com/alexbilbie). ## Requirements The latest version of this package supports the following versions of PHP: - PHP 8.2 - PHP 8.3 - PHP 8.4 - PHP 8.5 The `openssl` and `json` extensions are also required. All HTTP messages passed to the server should be [PSR-7 compliant](https://www.php-fig.org/psr/psr-7/). This ensures interoperability with other packages and frameworks. ## Installation ``` composer require league/oauth2-server ``` ## Documentation The library documentation can be found at [https://oauth2.thephpleague.com](https://oauth2.thephpleague.com). You can contribute to the documentation in the [gh-pages branch](https://github.com/thephpleague/oauth2-server/tree/gh-pages/). ## Testing The library uses [PHPUnit](https://phpunit.de/) for unit tests. ``` vendor/bin/phpunit ``` ## Continuous Integration We use [Github Actions](https://github.com/features/actions), [Scrutinizer](https://scrutinizer-ci.com/), and [StyleCI](https://styleci.io/) for continuous integration. Check out [our](https://github.com/thephpleague/oauth2-server/blob/master/.github/workflows/tests.yml) [configuration](https://github.com/thephpleague/oauth2-server/blob/master/.scrutinizer.yml) [files](https://github.com/thephpleague/oauth2-server/blob/master/.styleci.yml) if you'd like to know more. ## Community Integrations - [Drupal](https://www.drupal.org/project/simple_oauth) - [Laravel Passport](https://github.com/laravel/passport) - [OAuth 2 Server for CakePHP 3](https://github.com/uafrica/oauth-server) - [OAuth 2 Server for Mezzio](https://github.com/mezzio/mezzio-authentication-oauth2) - [OAuth 2 Server Bundle (Symfony)](https://github.com/thephpleague/oauth2-server-bundle) - [Heimdall for CodeIgniter 4](https://github.com/ezralazuardy/heimdall) ## Changelog See the [project changelog](https://github.com/thephpleague/oauth2-server/blob/master/CHANGELOG.md) ## Contributing Contributions are always welcome. Please see [CONTRIBUTING.md](https://github.com/thephpleague/oauth2-server/blob/master/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](https://github.com/thephpleague/oauth2-server/blob/master/CODE_OF_CONDUCT.md) for details. ## Support Bugs and feature request are tracked on [GitHub](https://github.com/thephpleague/oauth2-server/issues). If you have any questions about OAuth _please_ open a ticket here; please **don't** email the address below. ## Security If you discover any security related issues, please email `andrew@noexceptions.io` instead of using the issue tracker. ## License This package is released under the MIT License. See the bundled [LICENSE](https://github.com/thephpleague/oauth2-server/blob/master/LICENSE) file for details. ## Credits This code is principally developed and maintained by [Andy Millington](https://twitter.com/Sephster). Between 2012 and 2017 this library was developed and maintained by [Alex Bilbie](https://alexbilbie.com/). PHP OAuth 2.0 Server is one of many packages provided by The PHP League. To find out more, please visit [our website](https://thephpleague.com). Special thanks to [all of these awesome contributors](https://github.com/thephpleague/oauth2-server/contributors). Additional thanks go to the [Mozilla Secure Open Source Fund](https://wiki.mozilla.org/MOSS/Secure_Open_Source) for funding a security audit of this library. The initial code was developed as part of the [Linkey](http://linkey.blogs.lincoln.ac.uk) project which was funded by [JISC](http://jisc.ac.uk) under the Access and Identity Management programme. ================================================ FILE: composer.json ================================================ { "name": "league/oauth2-server", "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", "homepage": "https://oauth2.thephpleague.com/", "license": "MIT", "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "ext-openssl": "*", "league/event": "^3.0", "league/uri": "^7.8", "lcobucci/jwt": "^5.6", "psr/http-message": "^2.0", "defuse/php-encryption": "^2.4", "ext-json": "*", "lcobucci/clock": "^3.3", "psr/http-server-middleware": "^1.0" }, "require-dev": { "phpunit/phpunit": "^11.5.50", "laminas/laminas-diactoros": "^3.8", "phpstan/phpstan": "^2.1.38", "phpstan/phpstan-phpunit": "^2.0.12", "roave/security-advisories": "dev-master", "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "slevomat/coding-standard": "^8.27.1", "php-parallel-lint/php-parallel-lint": "^1.4", "squizlabs/php_codesniffer": "^4.0", "paragonie/random_compat": "^9.99.100" }, "repositories": [ { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git" } ], "keywords": [ "oauth", "oauth2", "oauth 2", "oauth 2.0", "server", "auth", "authorization", "authorisation", "authentication", "resource", "api", "protect", "secure" ], "authors": [ { "name": "Alex Bilbie", "email": "hello@alexbilbie.com", "homepage": "http://www.alexbilbie.com", "role": "Developer" }, { "name": "Andy Millington", "email": "andrew@noexceptions.io", "homepage": "https://www.noexceptions.io", "role": "Developer" } ], "replace": { "lncd/oauth2": "*", "league/oauth2server": "*" }, "autoload": { "psr-4": { "League\\OAuth2\\Server\\": "src/" } }, "autoload-dev": { "psr-4": { "LeagueTests\\": "tests/" } }, "config": { "allow-plugins": { "ocramius/package-versions": true, "phpstan/extension-installer": true, "dealerdirect/phpcodesniffer-composer-installer": false } } } ================================================ FILE: examples/README.md ================================================ # Example implementations (via [`Slim 3`](https://github.com/slimphp/Slim/tree/3.x)) ## Installation 0. Run `composer install` in this directory to install dependencies 0. Create a private key `openssl genrsa -out private.key 2048` 0. Export the public key `openssl rsa -in private.key -pubout > public.key` 0. Start local PHP server `php -S 127.0.0.1:4444 -t public/` ## Testing the client credentials grant example Send the following cURL request: ``` curl -X "POST" "http://localhost:4444/client_credentials.php/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: 1.0" \ --data-urlencode "grant_type=client_credentials" \ --data-urlencode "client_id=myawesomeapp" \ --data-urlencode "client_secret=abc123" \ --data-urlencode "scope=basic email" ``` ## Testing the password grant example Send the following cURL request: ``` curl -X "POST" "http://localhost:4444/password.php/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: 1.0" \ --data-urlencode "grant_type=password" \ --data-urlencode "client_id=myawesomeapp" \ --data-urlencode "client_secret=abc123" \ --data-urlencode "username=alex" \ --data-urlencode "password=whisky" \ --data-urlencode "scope=basic email" ``` ## Testing the refresh token grant example Send the following cURL request. Replace `{{REFRESH_TOKEN}}` with a refresh token from another grant above: ``` curl -X "POST" "http://localhost:4444/refresh_token.php/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: 1.0" \ --data-urlencode "grant_type=refresh_token" \ --data-urlencode "client_id=myawesomeapp" \ --data-urlencode "client_secret=abc123" \ --data-urlencode "refresh_token={{REFRESH_TOKEN}}" ``` ## Testing the device authorization grant example Send the following cURL request. This will return a device code which can be exchanged for an access token. ``` curl -X "POST" "http://localhost:4444/device_code.php/device_authorization" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: 1.0" \ --data-urlencode "client_id=myawesomeapp" \ --data-urlencode "client_secret=abc123" \ --data-urlencode "scope=basic email" ``` We have set up the example so that a user ID is already associated with the device code. In a production application you would implement an authorization view to allow a user to authorize the device. Issue the following cURL request to exchange your device code for an access token. Replace `{{DEVICE_CODE}}` with the device code returned from your first cURL post: ``` curl -X "POST" "http://localhost:4444/device_code.php/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: 1.0" \ --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \ --data-urlencode "device_code={{DEVICE_CODE}}" \ --data-urlencode "client_id=myawesomeapp" \ --data-urlencode "client_secret=abc123" ``` ================================================ FILE: examples/composer.json ================================================ { "require": { "slim/slim": "^3.12.3" }, "require-dev": { "league/event": "^3.0", "lcobucci/jwt": "^3.4.6 || ^4.0.4", "psr/http-message": "^1.0.1", "defuse/php-encryption": "^2.2.1", "laminas/laminas-diactoros": "^2.5.0" }, "autoload": { "psr-4": { "OAuth2ServerExamples\\": "src/", "League\\OAuth2\\Server\\": "../src/" } } } ================================================ FILE: examples/public/api.php ================================================ function () { $server = new ResourceServer( new AccessTokenRepository(), // instance of AccessTokenRepositoryInterface 'file://' . __DIR__ . '/../public.key' // the authorization server's public key ); return $server; }, ]); // Add the resource server middleware which will intercept and validate requests $app->add( new ResourceServerMiddleware( $app->getContainer()->get(ResourceServer::class) ) ); // An example endpoint secured with OAuth 2.0 $app->get( '/users', function (ServerRequestInterface $request, ResponseInterface $response) { $users = [ [ 'id' => 123, 'name' => 'Alex', 'email' => 'alex@thephpleague.com', ], [ 'id' => 124, 'name' => 'Frank', 'email' => 'frank@thephpleague.com', ], [ 'id' => 125, 'name' => 'Phil', 'email' => 'phil@thephpleague.com', ], ]; $totalUsers = count($users); // If the access token doesn't have the `basic` scope hide users' names if (in_array('basic', $request->getAttribute('oauth_scopes')) === false) { for ($i = 0; $i < $totalUsers; $i++) { unset($users[$i]['name']); } } // If the access token doesn't have the `email` scope hide users' email addresses if (in_array('email', $request->getAttribute('oauth_scopes')) === false) { for ($i = 0; $i < $totalUsers; $i++) { unset($users[$i]['email']); } } $response->getBody()->write(json_encode($users)); return $response->withStatus(200); } ); $app->run(); ================================================ FILE: examples/public/auth_code.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); include __DIR__ . '/../vendor/autoload.php'; use Laminas\Diactoros\Stream; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\AuthCodeGrant; use OAuth2ServerExamples\Entities\UserEntity; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\AuthCodeRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use OAuth2ServerExamples\Repositories\RefreshTokenRepository; use OAuth2ServerExamples\Repositories\ScopeRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); $scopeRepository = new ScopeRepository(); $accessTokenRepository = new AccessTokenRepository(); $authCodeRepository = new AuthCodeRepository(); $refreshTokenRepository = new RefreshTokenRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKeyPath, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' ); // Enable the authentication code grant on the server with a token TTL of 1 hour $server->enableGrantType( new AuthCodeGrant( $authCodeRepository, $refreshTokenRepository, new DateInterval('PT10M') ), new DateInterval('PT1H') ); return $server; }, ]); $app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Validate the HTTP request and return an AuthorizationRequest object. // The auth request object can be serialized into a user's session $authRequest = $server->validateAuthorizationRequest($request); // Once the user has logged in set the user on the AuthorizationRequest $authRequest->setUser(new UserEntity()); // Once the user has approved or denied the client update the status // (true = approved, false = denied) $authRequest->setAuthorizationApproved(true); // Return the HTTP redirect response return $server->completeAuthorizationRequest($authRequest, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->run(); ================================================ FILE: examples/public/client_credentials.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); include __DIR__ . '/../vendor/autoload.php'; use Laminas\Diactoros\Stream; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\ClientCredentialsGrant; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use OAuth2ServerExamples\Repositories\ScopeRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); // instance of ClientRepositoryInterface $scopeRepository = new ScopeRepository(); // instance of ScopeRepositoryInterface $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface // Path to public and private keys $privateKey = 'file://' . __DIR__ . '/../private.key'; //$privateKey = new CryptKey('file://path/to/private.key', 'passphrase'); // if private key has a pass phrase // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKey, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' ); // Enable the client credentials grant on the server $server->enableGrantType( new ClientCredentialsGrant(), new DateInterval('PT1H') // access tokens will expire after 1 hour ); return $server; }, ]); $app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Try to respond to the request return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { // All instances of OAuthServerException can be formatted into a HTTP response return $exception->generateHttpResponse($response); } catch (Exception $exception) { // Unknown exception $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->run(); ================================================ FILE: examples/public/device_code.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); include __DIR__ . '/../vendor/autoload.php'; use Laminas\Diactoros\Stream; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\DeviceCodeGrant; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use OAuth2ServerExamples\Repositories\DeviceCodeRepository; use OAuth2ServerExamples\Repositories\RefreshTokenRepository; use OAuth2ServerExamples\Repositories\ScopeRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); $scopeRepository = new ScopeRepository(); $accessTokenRepository = new AccessTokenRepository(); $refreshTokenRepository = new RefreshTokenRepository(); $deviceCodeRepository = new DeviceCodeRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; // Set up the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKeyPath, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' ); // Enable the device code grant on the server with a token TTL of 1 hour $server->enableGrantType( new DeviceCodeGrant( $deviceCodeRepository, $refreshTokenRepository, new DateInterval('PT10M'), 'http://foo/bar' ), new DateInterval('PT1H') ); return $server; }, ]); $app->post('/device_authorization', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { $deviceCodeResponse = $server->respondToDeviceAuthorizationRequest($request, $response); return $deviceCodeResponse; // Extract the device code. Usually we would then assign the user ID to // the device code but for the purposes of this example, we've hard // coded it in the response above. // $deviceCode = json_decode((string) $deviceCodeResponse->getBody()); // Once the user has logged in and approved the request, set the user on the device code // $server->completeDeviceAuthorizationRequest($deviceCode->user_code, 1); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->run(); ================================================ FILE: examples/public/implicit.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); include __DIR__ . '/../vendor/autoload.php'; use Laminas\Diactoros\Stream; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\ImplicitGrant; use OAuth2ServerExamples\Entities\UserEntity; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use OAuth2ServerExamples\Repositories\ScopeRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); $scopeRepository = new ScopeRepository(); $accessTokenRepository = new AccessTokenRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKeyPath, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' ); // Enable the implicit grant on the server with a token TTL of 1 hour $server->enableGrantType(new ImplicitGrant(new DateInterval('PT1H'))); return $server; }, ]); $app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Validate the HTTP request and return an AuthorizationRequest object. // The auth request object can be serialized into a user's session $authRequest = $server->validateAuthorizationRequest($request); // Once the user has logged in set the user on the AuthorizationRequest $authRequest->setUser(new UserEntity()); // Once the user has approved or denied the client update the status // (true = approved, false = denied) $authRequest->setAuthorizationApproved(true); // Return the HTTP redirect response return $server->completeAuthorizationRequest($authRequest, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->run(); ================================================ FILE: examples/public/middleware_use.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); include __DIR__ . '/../vendor/autoload.php'; use Laminas\Diactoros\Stream; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Grant\AuthCodeGrant; use League\OAuth2\Server\Grant\RefreshTokenGrant; use League\OAuth2\Server\Middleware\AuthorizationServerMiddleware; use League\OAuth2\Server\Middleware\ResourceServerMiddleware; use League\OAuth2\Server\ResourceServer; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\AuthCodeRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use OAuth2ServerExamples\Repositories\RefreshTokenRepository; use OAuth2ServerExamples\Repositories\ScopeRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); $accessTokenRepository = new AccessTokenRepository(); $scopeRepository = new ScopeRepository(); $authCodeRepository = new AuthCodeRepository(); $refreshTokenRepository = new RefreshTokenRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKeyPath, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' ); // Enable the authentication code grant on the server with a token TTL of 1 hour $server->enableGrantType( new AuthCodeGrant( $authCodeRepository, $refreshTokenRepository, new DateInterval('PT10M') ), new DateInterval('PT1H') ); // Enable the refresh token grant on the server with a token TTL of 1 month $server->enableGrantType( new RefreshTokenGrant($refreshTokenRepository), new DateInterval('P1M') ); return $server; }, ResourceServer::class => function () { $publicKeyPath = 'file://' . __DIR__ . '/../public.key'; $server = new ResourceServer( new AccessTokenRepository(), $publicKeyPath ); return $server; }, ]); // Access token issuer $app->post('/access_token', function (): void { })->add(new AuthorizationServerMiddleware($app->getContainer()->get(AuthorizationServer::class))); // Secured API $app->group('/api', function (): void { $app->get('/user', function (ServerRequestInterface $request, ResponseInterface $response) { $params = []; if (in_array('basic', $request->getAttribute('oauth_scopes', []))) { $params = [ 'id' => 1, 'name' => 'Alex', 'city' => 'London', ]; } if (in_array('email', $request->getAttribute('oauth_scopes', []))) { $params['email'] = 'alex@example.com'; } $body = new Stream('php://temp', 'r+'); $body->write(json_encode($params)); return $response->withBody($body); }); })->add(new ResourceServerMiddleware($app->getContainer()->get(ResourceServer::class))); $app->run(); ================================================ FILE: examples/public/password.php ================================================ function () { // Setup the authorization server $server = new AuthorizationServer( new ClientRepository(), // instance of ClientRepositoryInterface new AccessTokenRepository(), // instance of AccessTokenRepositoryInterface new ScopeRepository(), // instance of ScopeRepositoryInterface 'file://' . __DIR__ . '/../private.key', // path to private key 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' // encryption key ); $grant = new PasswordGrant( new UserRepository(), // instance of UserRepositoryInterface new RefreshTokenRepository() // instance of RefreshTokenRepositoryInterface ); $grant->setRefreshTokenTTL(new DateInterval('P1M')); // refresh tokens will expire after 1 month // Enable the password grant on the server with a token TTL of 1 hour $server->enableGrantType( $grant, new DateInterval('PT1H') // access tokens will expire after 1 hour ); return $server; }, ]); $app->post( '/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Try to respond to the access token request return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { // All instances of OAuthServerException can be converted to a PSR-7 response return $exception->generateHttpResponse($response); } catch (Exception $exception) { // Catch unexpected exceptions $body = $response->getBody(); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } } ); $app->run(); ================================================ FILE: examples/public/refresh_token.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); include __DIR__ . '/../vendor/autoload.php'; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\RefreshTokenGrant; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use OAuth2ServerExamples\Repositories\RefreshTokenRepository; use OAuth2ServerExamples\Repositories\ScopeRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); $accessTokenRepository = new AccessTokenRepository(); $scopeRepository = new ScopeRepository(); $refreshTokenRepository = new RefreshTokenRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKeyPath, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' ); // Enable the refresh token grant on the server $grant = new RefreshTokenGrant($refreshTokenRepository); $grant->setRefreshTokenTTL(new DateInterval('P1M')); // The refresh token will expire in 1 month $server->enableGrantType( $grant, new DateInterval('PT1H') // The new access token will expire after 1 hour ); return $server; }, ]); $app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (Exception $exception) { $response->getBody()->write($exception->getMessage()); return $response->withStatus(500); } }); $app->run(); ================================================ FILE: examples/src/Entities/AccessTokenEntity.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\Traits\AccessTokenTrait; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; class AccessTokenEntity implements AccessTokenEntityInterface { use AccessTokenTrait; use TokenEntityTrait; use EntityTrait; } ================================================ FILE: examples/src/Entities/AuthCodeEntity.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Entities\Traits\AuthCodeTrait; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; class AuthCodeEntity implements AuthCodeEntityInterface { use EntityTrait; use TokenEntityTrait; use AuthCodeTrait; } ================================================ FILE: examples/src/Entities/ClientEntity.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\Traits\ClientTrait; use League\OAuth2\Server\Entities\Traits\EntityTrait; class ClientEntity implements ClientEntityInterface { use EntityTrait; use ClientTrait; public function setName(string $name): void { $this->name = $name; } public function setRedirectUri(string $uri): void { $this->redirectUri = $uri; } public function setConfidential(): void { $this->isConfidential = true; } } ================================================ FILE: examples/src/Entities/DeviceCodeEntity.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; use League\OAuth2\Server\Entities\Traits\DeviceCodeTrait; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; class DeviceCodeEntity implements DeviceCodeEntityInterface { use EntityTrait; use DeviceCodeTrait; use TokenEntityTrait; } ================================================ FILE: examples/src/Entities/RefreshTokenEntity.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait; class RefreshTokenEntity implements RefreshTokenEntityInterface { use RefreshTokenTrait; use EntityTrait; } ================================================ FILE: examples/src/Entities/ScopeEntity.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\ScopeTrait; class ScopeEntity implements ScopeEntityInterface { use EntityTrait; use ScopeTrait; } ================================================ FILE: examples/src/Entities/UserEntity.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\UserEntityInterface; class UserEntity implements UserEntityInterface { /** * Return the user's identifier. */ public function getIdentifier(): string { return '1'; } } ================================================ FILE: examples/src/Repositories/AccessTokenRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Repositories; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use OAuth2ServerExamples\Entities\AccessTokenEntity; class AccessTokenRepository implements AccessTokenRepositoryInterface { /** * {@inheritdoc} */ public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void { // Some logic here to save the access token to a database } /** * {@inheritdoc} */ public function revokeAccessToken($tokenId): void { // Some logic here to revoke the access token } /** * {@inheritdoc} */ public function isAccessTokenRevoked($tokenId): bool { return false; // Access token hasn't been revoked } /** * {@inheritdoc} */ public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null): AccessTokenEntityInterface { $accessToken = new AccessTokenEntity(); $accessToken->setClient($clientEntity); foreach ($scopes as $scope) { $accessToken->addScope($scope); } if ($userIdentifier !== null) { $accessToken->setUserIdentifier((string) $userIdentifier); } return $accessToken; } } ================================================ FILE: examples/src/Repositories/AuthCodeRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Repositories; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; use OAuth2ServerExamples\Entities\AuthCodeEntity; class AuthCodeRepository implements AuthCodeRepositoryInterface { /** * {@inheritdoc} */ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void { // Some logic to persist the auth code to a database } /** * {@inheritdoc} */ public function revokeAuthCode($codeId): void { // Some logic to revoke the auth code in a database } /** * {@inheritdoc} */ public function isAuthCodeRevoked($codeId): bool { return false; // The auth code has not been revoked } /** * {@inheritdoc} */ public function getNewAuthCode(): AuthCodeEntityInterface { return new AuthCodeEntity(); } } ================================================ FILE: examples/src/Repositories/ClientRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use OAuth2ServerExamples\Entities\ClientEntity; use function array_key_exists; use function password_hash; use function password_verify; class ClientRepository implements ClientRepositoryInterface { private const CLIENT_NAME = 'My Awesome App'; private const REDIRECT_URI = 'http://foo/bar'; /** * {@inheritdoc} */ public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface { $client = new ClientEntity(); $client->setIdentifier($clientIdentifier); $client->setName(self::CLIENT_NAME); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); return $client; } /** * {@inheritdoc} */ public function validateClient($clientIdentifier, $clientSecret, $grantType): bool { $clients = [ 'myawesomeapp' => [ 'secret' => password_hash('abc123', PASSWORD_BCRYPT), 'name' => self::CLIENT_NAME, 'redirect_uri' => self::REDIRECT_URI, 'is_confidential' => true, ], ]; // Check if client is registered if (array_key_exists($clientIdentifier, $clients) === false) { return false; } if (password_verify($clientSecret, $clients[$clientIdentifier]['secret']) === false) { return false; } return true; } } ================================================ FILE: examples/src/Repositories/DeviceCodeRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Repositories; use DateTimeImmutable; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface; use OAuth2ServerExamples\Entities\ClientEntity; use OAuth2ServerExamples\Entities\DeviceCodeEntity; use OAuth2ServerExamples\Entities\ScopeEntity; class DeviceCodeRepository implements DeviceCodeRepositoryInterface { /** * {@inheritdoc} */ public function getNewDeviceCode(): DeviceCodeEntityInterface { return new DeviceCodeEntity(); } /** * {@inheritdoc} */ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): void { // Some logic to persist a new device code to a database } /** * {@inheritdoc} */ public function getDeviceCodeEntityByDeviceCode($deviceCode): ?DeviceCodeEntityInterface { $clientEntity = new ClientEntity(); $clientEntity->setIdentifier('myawesomeapp'); $deviceCodeEntity = new DeviceCodeEntity(); $deviceCodeEntity->setIdentifier($deviceCode); $deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('now +1 hour')); $deviceCodeEntity->setClient($clientEntity); $deviceCodeEntity->setLastPolledAt(new DateTimeImmutable()); $scopes = []; foreach ($scopes as $scope) { $scopeEntity = new ScopeEntity(); $scopeEntity->setIdentifier($scope); $deviceCodeEntity->addScope($scopeEntity); } // The user identifier should be set when the user authenticates on the // OAuth server, along with whether they approved the request $deviceCodeEntity->setUserApproved(true); $deviceCodeEntity->setUserIdentifier('1'); return $deviceCodeEntity; } /** * {@inheritdoc} */ public function revokeDeviceCode($codeId): void { // Some logic to revoke device code } /** * {@inheritdoc} */ public function isDeviceCodeRevoked($codeId): bool { return false; } } ================================================ FILE: examples/src/Repositories/RefreshTokenRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Repositories; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use OAuth2ServerExamples\Entities\RefreshTokenEntity; class RefreshTokenRepository implements RefreshTokenRepositoryInterface { /** * {@inheritdoc} */ public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void { // Some logic to persist the refresh token in a database } /** * {@inheritdoc} */ public function revokeRefreshToken($tokenId): void { // Some logic to revoke the refresh token in a database } /** * {@inheritdoc} */ public function isRefreshTokenRevoked($tokenId): bool { return false; // The refresh token has not been revoked } /** * {@inheritdoc} */ public function getNewRefreshToken(): ?RefreshTokenEntityInterface { return new RefreshTokenEntity(); } } ================================================ FILE: examples/src/Repositories/ScopeRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use OAuth2ServerExamples\Entities\ScopeEntity; use function array_key_exists; class ScopeRepository implements ScopeRepositoryInterface { /** * {@inheritdoc} */ public function getScopeEntityByIdentifier(string $identifier): ?ScopeEntityInterface { $scopes = [ 'basic' => [ 'description' => 'Basic details about you', ], 'email' => [ 'description' => 'Your email address', ], ]; if (array_key_exists($identifier, $scopes) === false) { return null; } $scope = new ScopeEntity(); $scope->setIdentifier($identifier); return $scope; } /** * {@inheritdoc} */ public function finalizeScopes( array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null, $authCodeId = null ): array { // Example of programmatically modifying the final scope of the access token if ((int) $userIdentifier === 1) { $scope = new ScopeEntity(); $scope->setIdentifier('email'); $scopes[] = $scope; } return $scopes; } } ================================================ FILE: examples/src/Repositories/UserRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace OAuth2ServerExamples\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use OAuth2ServerExamples\Entities\UserEntity; class UserRepository implements UserRepositoryInterface { /** * {@inheritdoc} */ public function getUserEntityByUserCredentials( $username, $password, $grantType, ClientEntityInterface $clientEntity ): ?UserEntityInterface { if ($username === 'alex' && $password === 'whisky') { return new UserEntity(); } return null; } } ================================================ FILE: phpcs.xml.dist ================================================ src tests examples examples/vendor/* ================================================ FILE: phpstan.neon.dist ================================================ parameters: level: 8 paths: - src - tests ignoreErrors: - message: '#Deprecated since v5.5, please use {@see self::withValidationConstraints\(\)} instead#' reportUnmatched: false ================================================ FILE: phpunit.xml.dist ================================================ src ./tests/ ================================================ FILE: src/AuthorizationServer.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server; use DateInterval; use Defuse\Crypto\Key; use League\OAuth2\Server\EventEmitting\EmitterAwareInterface; use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; use League\OAuth2\Server\ResponseTypes\AbstractResponseType; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use SensitiveParameter; class AuthorizationServer implements EmitterAwareInterface { use EmitterAwarePolyfill; /** * @var GrantTypeInterface[] */ protected array $enabledGrantTypes = []; /** * @var DateInterval[] */ protected array $grantTypeAccessTokenTTL = []; protected CryptKeyInterface $privateKey; protected CryptKeyInterface $publicKey; protected ResponseTypeInterface $responseType; private string|Key $encryptionKey; private string $defaultScope = ''; private bool $revokeRefreshTokens = true; /** * New server instance */ public function __construct( private ClientRepositoryInterface $clientRepository, private AccessTokenRepositoryInterface $accessTokenRepository, private ScopeRepositoryInterface $scopeRepository, #[SensitiveParameter] CryptKeyInterface|string $privateKey, #[SensitiveParameter] Key|string $encryptionKey, ResponseTypeInterface|null $responseType = null ) { if ($privateKey instanceof CryptKeyInterface === false) { $privateKey = new CryptKey($privateKey); } $this->privateKey = $privateKey; $this->encryptionKey = $encryptionKey; if ($responseType === null) { $responseType = new BearerTokenResponse(); } else { $responseType = clone $responseType; } $this->responseType = $responseType; } /** * Enable a grant type on the server */ public function enableGrantType(GrantTypeInterface $grantType, DateInterval|null $accessTokenTTL = null): void { if ($accessTokenTTL === null) { $accessTokenTTL = new DateInterval('PT1H'); } $grantType->setAccessTokenRepository($this->accessTokenRepository); $grantType->setClientRepository($this->clientRepository); $grantType->setScopeRepository($this->scopeRepository); $grantType->setDefaultScope($this->defaultScope); $grantType->setPrivateKey($this->privateKey); $grantType->setEmitter($this->getEmitter()); $grantType->setEncryptionKey($this->encryptionKey); $grantType->revokeRefreshTokens($this->revokeRefreshTokens); $this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType; $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL; } /** * Validate an authorization request * * @throws OAuthServerException */ public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface { foreach ($this->enabledGrantTypes as $grantType) { if ($grantType->canRespondToAuthorizationRequest($request)) { return $grantType->validateAuthorizationRequest($request); } } throw OAuthServerException::unsupportedGrantType(); } /** * Complete an authorization request */ public function completeAuthorizationRequest( AuthorizationRequestInterface $authRequest, ResponseInterface $response ): ResponseInterface { return $this->enabledGrantTypes[$authRequest->getGrantTypeId()] ->completeAuthorizationRequest($authRequest) ->generateHttpResponse($response); } /** * Respond to device authorization request * * @throws OAuthServerException */ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { foreach ($this->enabledGrantTypes as $grantType) { if ($grantType->canRespondToDeviceAuthorizationRequest($request)) { return $grantType ->respondToDeviceAuthorizationRequest($request) ->generateHttpResponse($response); } } throw OAuthServerException::unsupportedGrantType(); } /** * Complete a device authorization request */ public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void { $this->enabledGrantTypes['urn:ietf:params:oauth:grant-type:device_code'] ->completeDeviceAuthorizationRequest($deviceCode, $userId, $userApproved); } /** * Return an access token response. * * @throws OAuthServerException */ public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { foreach ($this->enabledGrantTypes as $grantType) { if (!$grantType->canRespondToAccessTokenRequest($request)) { continue; } $tokenResponse = $grantType->respondToAccessTokenRequest( $request, $this->getResponseType(), $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] ); return $tokenResponse->generateHttpResponse($response); } throw OAuthServerException::unsupportedGrantType(); } /** * Get the token type that grants will return in the HTTP response. */ protected function getResponseType(): ResponseTypeInterface { $responseType = clone $this->responseType; if ($responseType instanceof AbstractResponseType) { $responseType->setPrivateKey($this->privateKey); } $responseType->setEncryptionKey($this->encryptionKey); return $responseType; } /** * Set the default scope for the authorization server. */ public function setDefaultScope(string $defaultScope): void { $this->defaultScope = $defaultScope; } /** * Sets whether to revoke refresh tokens or not (for all grant types). */ public function revokeRefreshTokens(bool $revokeRefreshTokens): void { $this->revokeRefreshTokens = $revokeRefreshTokens; } } ================================================ FILE: src/AuthorizationValidators/AuthorizationValidatorInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\AuthorizationValidators; use Psr\Http\Message\ServerRequestInterface; interface AuthorizationValidatorInterface { /** * Determine the access token in the authorization header and append OAUth * properties to the request as attributes. */ public function validateAuthorization(ServerRequestInterface $request): ServerRequestInterface; } ================================================ FILE: src/AuthorizationValidators/BearerTokenValidator.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\AuthorizationValidators; use DateInterval; use DateTimeZone; use Lcobucci\Clock\SystemClock; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Exception; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\UnencryptedToken; use Lcobucci\JWT\Validation\Constraint\LooseValidAt; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; use function date_default_timezone_get; use function preg_replace; use function trim; class BearerTokenValidator implements AuthorizationValidatorInterface { use CryptTrait; protected CryptKeyInterface $publicKey; private Configuration $jwtConfiguration; public function __construct(private AccessTokenRepositoryInterface $accessTokenRepository, private ?DateInterval $jwtValidAtDateLeeway = null) { } /** * Set the public key */ public function setPublicKey(CryptKeyInterface $key): void { $this->publicKey = $key; $this->initJwtConfiguration(); } /** * Initialise the JWT configuration. */ private function initJwtConfiguration(): void { $this->jwtConfiguration = Configuration::forSymmetricSigner( new Sha256(), InMemory::plainText('empty', 'empty') ); $clock = new SystemClock(new DateTimeZone(date_default_timezone_get())); $publicKeyContents = $this->publicKey->getKeyContents(); if ($publicKeyContents === '') { throw new RuntimeException('Public key is empty'); } // TODO: next major release: replace deprecated method and remove phpstan ignored error $this->jwtConfiguration->setValidationConstraints( new LooseValidAt($clock, $this->jwtValidAtDateLeeway), new SignedWith( new Sha256(), InMemory::plainText($publicKeyContents, $this->publicKey->getPassPhrase() ?? '') ) ); } /** * {@inheritdoc} */ public function validateAuthorization(ServerRequestInterface $request): ServerRequestInterface { if ($request->hasHeader('authorization') === false) { throw OAuthServerException::accessDenied('Missing "Authorization" header'); } $header = $request->getHeader('authorization'); $jwt = trim((string) preg_replace('/^\s*Bearer\s/i', '', $header[0])); if ($jwt === '') { throw OAuthServerException::accessDenied('Missing "Bearer" token'); } try { // Attempt to parse the JWT $token = $this->jwtConfiguration->parser()->parse($jwt); } catch (Exception $exception) { throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception); } try { // Attempt to validate the JWT $constraints = $this->jwtConfiguration->validationConstraints(); $this->jwtConfiguration->validator()->assert($token, ...$constraints); } catch (RequiredConstraintsViolated $exception) { throw OAuthServerException::accessDenied('Access token could not be verified', null, $exception); } if (!$token instanceof UnencryptedToken) { throw OAuthServerException::accessDenied('Access token is not an instance of UnencryptedToken'); } $claims = $token->claims(); // Check if token has been revoked if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) { throw OAuthServerException::accessDenied('Access token has been revoked'); } // Return the request with additional attributes return $request ->withAttribute('oauth_access_token_id', $claims->get('jti')) ->withAttribute('oauth_client_id', $claims->get('aud')[0]) ->withAttribute('oauth_user_id', $claims->get('sub')) ->withAttribute('oauth_scopes', $claims->get('scopes')); } } ================================================ FILE: src/CodeChallengeVerifiers/CodeChallengeVerifierInterface.php ================================================ * @copyright Copyright (c) Lukáš Unger * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\CodeChallengeVerifiers; interface CodeChallengeVerifierInterface { /** * Return code challenge method. */ public function getMethod(): string; /** * Verify the code challenge. */ public function verifyCodeChallenge(string $codeVerifier, string $codeChallenge): bool; } ================================================ FILE: src/CodeChallengeVerifiers/PlainVerifier.php ================================================ * @copyright Copyright (c) Lukáš Unger * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\CodeChallengeVerifiers; use function hash_equals; class PlainVerifier implements CodeChallengeVerifierInterface { /** * Return code challenge method. */ public function getMethod(): string { return 'plain'; } /** * Verify the code challenge. */ public function verifyCodeChallenge(string $codeVerifier, string $codeChallenge): bool { return hash_equals($codeVerifier, $codeChallenge); } } ================================================ FILE: src/CodeChallengeVerifiers/S256Verifier.php ================================================ * @copyright Copyright (c) Lukáš Unger * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\CodeChallengeVerifiers; use function base64_encode; use function hash; use function hash_equals; use function rtrim; use function strtr; class S256Verifier implements CodeChallengeVerifierInterface { /** * Return code challenge method. */ public function getMethod(): string { return 'S256'; } /** * Verify the code challenge. */ public function verifyCodeChallenge(string $codeVerifier, string $codeChallenge): bool { return hash_equals( strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'), $codeChallenge ); } } ================================================ FILE: src/CryptKey.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server; use LogicException; use OpenSSLAsymmetricKey; use SensitiveParameter; use function decoct; use function file_get_contents; use function fileperms; use function in_array; use function is_file; use function is_readable; use function openssl_pkey_get_details; use function openssl_pkey_get_private; use function openssl_pkey_get_public; use function sprintf; use function trigger_error; class CryptKey implements CryptKeyInterface { private const FILE_PREFIX = 'file://'; /** * @var string Key contents */ protected string $keyContents; protected string $keyPath; public function __construct( string $keyPath, #[SensitiveParameter] protected ?string $passPhrase = null, bool $keyPermissionsCheck = true ) { if (str_starts_with($keyPath, self::FILE_PREFIX) === false && $this->isValidKey($keyPath, $this->passPhrase ?? '')) { $this->keyContents = $keyPath; $this->keyPath = ''; // There's no file, so no need for permission check. $keyPermissionsCheck = false; } elseif (is_file($keyPath)) { if (str_starts_with($keyPath, self::FILE_PREFIX) === false) { $keyPath = self::FILE_PREFIX . $keyPath; } if (!is_readable($keyPath)) { throw new LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath)); } $keyContents = file_get_contents($keyPath); if ($keyContents === false) { throw new LogicException('Unable to read key from file ' . $keyPath); } $this->keyContents = $keyContents; $this->keyPath = $keyPath; if (!$this->isValidKey($this->keyContents, $this->passPhrase ?? '')) { throw new LogicException('Unable to read key from file ' . $keyPath); } } else { throw new LogicException('Invalid key supplied'); } if ($keyPermissionsCheck === true && PHP_OS_FAMILY !== 'Windows') { // Verify the permissions of the key $keyPathPerms = decoct(fileperms($this->keyPath) & 0777); if (in_array($keyPathPerms, ['400', '440', '600', '640', '660'], true) === false) { trigger_error( sprintf( 'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s', $this->keyPath, $keyPathPerms ), E_USER_NOTICE ); } } } /** * {@inheritdoc} */ public function getKeyContents(): string { return $this->keyContents; } /** * Validate key contents. */ private function isValidKey( #[SensitiveParameter] string $contents, #[SensitiveParameter] string $passPhrase ): bool { $privateKey = openssl_pkey_get_private($contents, $passPhrase); $key = $privateKey instanceof OpenSSLAsymmetricKey ? $privateKey : openssl_pkey_get_public($contents); if ($key === false) { return false; } $details = openssl_pkey_get_details($key); return $details !== false && in_array( $details['type'] ?? -1, [OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC], true ); } /** * {@inheritdoc} */ public function getKeyPath(): string { return $this->keyPath; } /** * {@inheritdoc} */ public function getPassPhrase(): ?string { return $this->passPhrase; } } ================================================ FILE: src/CryptKeyInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server; use Defuse\Crypto\Crypto; use Defuse\Crypto\Exception\EnvironmentIsBrokenException; use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; use Defuse\Crypto\Key; use Exception; use InvalidArgumentException; use LogicException; use SensitiveParameter; use function is_string; trait CryptTrait { protected string|Key|null $encryptionKey = null; /** * Encrypt data with encryptionKey. * * @throws LogicException */ protected function encrypt(string $unencryptedData): string { try { if ($this->encryptionKey instanceof Key) { return Crypto::encrypt($unencryptedData, $this->encryptionKey); } if (is_string($this->encryptionKey)) { return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey); } throw new LogicException('Encryption key not set when attempting to encrypt'); } catch (Exception $e) { throw new LogicException($e->getMessage(), 0, $e); } } /** * Decrypt data with encryptionKey. * * @throws LogicException */ protected function decrypt(string $encryptedData): string { try { if ($this->encryptionKey instanceof Key) { return Crypto::decrypt($encryptedData, $this->encryptionKey); } if (is_string($this->encryptionKey)) { return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey); } throw new LogicException('Encryption key not set when attempting to decrypt'); } catch (WrongKeyOrModifiedCiphertextException $e) { $exceptionMessage = 'The authcode or decryption key/password used ' . 'is not correct'; throw new InvalidArgumentException($exceptionMessage, 0, $e); } catch (EnvironmentIsBrokenException $e) { $exceptionMessage = 'Auth code decryption failed. This is likely ' . 'due to an environment issue or runtime bug in the ' . 'decryption library'; throw new LogicException($exceptionMessage, 0, $e); } catch (Exception $e) { throw new LogicException($e->getMessage(), 0, $e); } } public function setEncryptionKey( #[SensitiveParameter] Key|string|null $key = null ): void { $this->encryptionKey = $key; } } ================================================ FILE: src/Entities/AccessTokenEntityInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; use League\OAuth2\Server\CryptKeyInterface; interface AccessTokenEntityInterface extends TokenInterface { /** * Set a private key used to encrypt the access token. */ public function setPrivateKey(CryptKeyInterface $privateKey): void; /** * Generate a string representation of the access token. */ public function toString(): string; } ================================================ FILE: src/Entities/AuthCodeEntityInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; interface AuthCodeEntityInterface extends TokenInterface { public function getRedirectUri(): string|null; public function setRedirectUri(string $uri): void; } ================================================ FILE: src/Entities/ClientEntityInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; interface ClientEntityInterface { /** * Get the client's identifier. * * @return non-empty-string */ public function getIdentifier(): string; /** * Get the client's name. */ public function getName(): string; /** * Returns the registered redirect URI (as a string). Alternatively return * an indexed array of redirect URIs. * * @return string|string[] */ public function getRedirectUri(): string|array; /** * Returns true if the client is confidential. */ public function isConfidential(): bool; /* * Returns true if the client supports the given grant type. * * TODO: To be added in a future major release. */ // public function supportsGrantType(string $grantType): bool; } ================================================ FILE: src/Entities/DeviceCodeEntityInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; use DateTimeImmutable; interface DeviceCodeEntityInterface extends TokenInterface { public function getUserCode(): string; public function setUserCode(string $userCode): void; public function getVerificationUri(): string; public function setVerificationUri(string $verificationUri): void; public function getVerificationUriComplete(): string; public function getLastPolledAt(): ?DateTimeImmutable; public function setLastPolledAt(DateTimeImmutable $lastPolledAt): void; public function getInterval(): int; public function setInterval(int $interval): void; public function getUserApproved(): bool; public function setUserApproved(bool $userApproved): void; } ================================================ FILE: src/Entities/RefreshTokenEntityInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; use DateTimeImmutable; interface RefreshTokenEntityInterface { /** * Get the token's identifier. * * @return non-empty-string */ public function getIdentifier(): string; /** * Set the token's identifier. * * @param non-empty-string $identifier */ public function setIdentifier(string $identifier): void; /** * Get the token's expiry date time. */ public function getExpiryDateTime(): DateTimeImmutable; /** * Set the date time when the token expires. */ public function setExpiryDateTime(DateTimeImmutable $dateTime): void; /** * Set the access token that the refresh token was associated with. */ public function setAccessToken(AccessTokenEntityInterface $accessToken): void; /** * Get the access token that the refresh token was originally associated with. */ public function getAccessToken(): AccessTokenEntityInterface; } ================================================ FILE: src/Entities/ScopeEntityInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; use JsonSerializable; interface ScopeEntityInterface extends JsonSerializable { /** * Get the scope's identifier. * * @return non-empty-string */ public function getIdentifier(): string; } ================================================ FILE: src/Entities/TokenInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; use DateTimeImmutable; interface TokenInterface { /** * Get the token's identifier. * * @return non-empty-string */ public function getIdentifier(): string; /** * Set the token's identifier. * * @param non-empty-string $identifier */ public function setIdentifier(string $identifier): void; /** * Get the token's expiry date time. */ public function getExpiryDateTime(): DateTimeImmutable; /** * Set the date time when the token expires. */ public function setExpiryDateTime(DateTimeImmutable $dateTime): void; /** * Set the identifier of the user associated with the token. * * @param non-empty-string $identifier */ public function setUserIdentifier(string $identifier): void; /** * Get the token user's identifier. * * @return non-empty-string|null */ public function getUserIdentifier(): string|null; /** * Get the client that the token was issued to. */ public function getClient(): ClientEntityInterface; /** * Set the client that the token was issued to. */ public function setClient(ClientEntityInterface $client): void; /** * Associate a scope with the token. */ public function addScope(ScopeEntityInterface $scope): void; /** * Return an array of scopes associated with the token. * * @return ScopeEntityInterface[] */ public function getScopes(): array; } ================================================ FILE: src/Entities/Traits/AccessTokenTrait.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; use DateTimeImmutable; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use RuntimeException; use SensitiveParameter; trait AccessTokenTrait { private CryptKeyInterface $privateKey; private Configuration $jwtConfiguration; /** * Set the private key used to encrypt this access token. */ public function setPrivateKey( #[SensitiveParameter] CryptKeyInterface $privateKey ): void { $this->privateKey = $privateKey; } /** * Initialise the JWT Configuration. */ public function initJwtConfiguration(): void { $privateKeyContents = $this->privateKey->getKeyContents(); if ($privateKeyContents === '') { throw new RuntimeException('Private key is empty'); } $this->jwtConfiguration = Configuration::forAsymmetricSigner( new Sha256(), InMemory::plainText($privateKeyContents, $this->privateKey->getPassPhrase() ?? ''), InMemory::plainText('empty', 'empty') ); } /** * Generate a JWT from the access token */ private function convertToJWT(): Token { $this->initJwtConfiguration(); return $this->jwtConfiguration->builder() ->permittedFor($this->getClient()->getIdentifier()) ->identifiedBy($this->getIdentifier()) ->issuedAt(new DateTimeImmutable()) ->canOnlyBeUsedAfter(new DateTimeImmutable()) ->expiresAt($this->getExpiryDateTime()) ->relatedTo($this->getSubjectIdentifier()) ->withClaim('scopes', $this->getScopes()) ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey()); } /** * Generate a string representation from the access token */ public function toString(): string { return $this->convertToJWT()->toString(); } abstract public function getClient(): ClientEntityInterface; abstract public function getExpiryDateTime(): DateTimeImmutable; /** * @return non-empty-string|null */ abstract public function getUserIdentifier(): string|null; /** * @return ScopeEntityInterface[] */ abstract public function getScopes(): array; /** * @return non-empty-string */ abstract public function getIdentifier(): string; /** * @return non-empty-string */ private function getSubjectIdentifier(): string { return $this->getUserIdentifier() ?? $this->getClient()->getIdentifier(); } } ================================================ FILE: src/Entities/Traits/AuthCodeTrait.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; trait AuthCodeTrait { protected ?string $redirectUri = null; public function getRedirectUri(): string|null { return $this->redirectUri; } public function setRedirectUri(string $uri): void { $this->redirectUri = $uri; } } ================================================ FILE: src/Entities/Traits/ClientTrait.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; trait ClientTrait { protected string $name; /** * @var string|string[] */ protected string|array $redirectUri; protected bool $isConfidential = false; /** * Get the client's name. * * * @codeCoverageIgnore */ public function getName(): string { return $this->name; } /** * Returns the registered redirect URI (as a string). Alternatively return * an indexed array of redirect URIs. * * @return string|string[] */ public function getRedirectUri(): string|array { return $this->redirectUri; } /** * Returns true if the client is confidential. */ public function isConfidential(): bool { return $this->isConfidential; } /** * Returns true if the client supports the given grant type. */ public function supportsGrantType(string $grantType): bool { return true; } } ================================================ FILE: src/Entities/Traits/DeviceCodeTrait.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; use DateTimeImmutable; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; trait DeviceCodeTrait { private bool $userApproved = false; private bool $includeVerificationUriComplete = false; private int $interval = 5; private string $userCode; private string $verificationUri; private ?DateTimeImmutable $lastPolledAt = null; public function getUserCode(): string { return $this->userCode; } public function setUserCode(string $userCode): void { $this->userCode = $userCode; } public function getVerificationUri(): string { return $this->verificationUri; } public function setVerificationUri(string $verificationUri): void { $this->verificationUri = $verificationUri; } public function getVerificationUriComplete(): string { return $this->verificationUri . '?user_code=' . $this->userCode; } abstract public function getClient(): ClientEntityInterface; abstract public function getExpiryDateTime(): DateTimeImmutable; /** * @return ScopeEntityInterface[] */ abstract public function getScopes(): array; /** * @return non-empty-string */ abstract public function getIdentifier(): string; public function getLastPolledAt(): ?DateTimeImmutable { return $this->lastPolledAt; } public function setLastPolledAt(DateTimeImmutable $lastPolledAt): void { $this->lastPolledAt = $lastPolledAt; } public function getInterval(): int { return $this->interval; } public function setInterval(int $interval): void { $this->interval = $interval; } public function getUserApproved(): bool { return $this->userApproved; } public function setUserApproved(bool $userApproved): void { $this->userApproved = $userApproved; } public function getVerificationUriCompleteInAuthResponse(): bool { return $this->includeVerificationUriComplete; } public function setVerificationUriCompleteInAuthResponse(bool $includeVerificationUriComplete): void { $this->includeVerificationUriComplete = $includeVerificationUriComplete; } } ================================================ FILE: src/Entities/Traits/EntityTrait.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; trait EntityTrait { /** * @var non-empty-string */ protected string $identifier; /** * @return non-empty-string */ public function getIdentifier(): string { return $this->identifier; } /** * @param non-empty-string $identifier */ public function setIdentifier(string $identifier): void { $this->identifier = $identifier; } } ================================================ FILE: src/Entities/Traits/RefreshTokenTrait.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; use DateTimeImmutable; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; trait RefreshTokenTrait { protected AccessTokenEntityInterface $accessToken; protected DateTimeImmutable $expiryDateTime; /** * {@inheritdoc} */ public function setAccessToken(AccessTokenEntityInterface $accessToken): void { $this->accessToken = $accessToken; } /** * {@inheritdoc} */ public function getAccessToken(): AccessTokenEntityInterface { return $this->accessToken; } /** * Get the token's expiry date time. */ public function getExpiryDateTime(): DateTimeImmutable { return $this->expiryDateTime; } /** * Set the date time when the token expires. */ public function setExpiryDateTime(DateTimeImmutable $dateTime): void { $this->expiryDateTime = $dateTime; } } ================================================ FILE: src/Entities/Traits/ScopeTrait.php ================================================ * @copyright Copyright (c) Andrew Millington * @license http://mit-license.org * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; trait ScopeTrait { /** * Serialize the object to the scopes string identifier when using json_encode(). */ public function jsonSerialize(): string { return $this->getIdentifier(); } abstract public function getIdentifier(): string; } ================================================ FILE: src/Entities/Traits/TokenEntityTrait.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities\Traits; use DateTimeImmutable; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use function array_values; trait TokenEntityTrait { /** * @var ScopeEntityInterface[] */ protected array $scopes = []; protected DateTimeImmutable $expiryDateTime; /** * @var non-empty-string|null */ protected string|null $userIdentifier = null; protected ClientEntityInterface $client; /** * Associate a scope with the token. */ public function addScope(ScopeEntityInterface $scope): void { $this->scopes[$scope->getIdentifier()] = $scope; } /** * Return an array of scopes associated with the token. * * @return ScopeEntityInterface[] */ public function getScopes(): array { return array_values($this->scopes); } /** * Get the token's expiry date time. */ public function getExpiryDateTime(): DateTimeImmutable { return $this->expiryDateTime; } /** * Set the date time when the token expires. */ public function setExpiryDateTime(DateTimeImmutable $dateTime): void { $this->expiryDateTime = $dateTime; } /** * Set the identifier of the user associated with the token. * * @param non-empty-string $identifier The identifier of the user */ public function setUserIdentifier(string $identifier): void { $this->userIdentifier = $identifier; } /** * Get the token user's identifier. * * @return non-empty-string|null */ public function getUserIdentifier(): string|null { return $this->userIdentifier; } /** * Get the client that the token was issued to. */ public function getClient(): ClientEntityInterface { return $this->client; } /** * Set the client that the token was issued to. */ public function setClient(ClientEntityInterface $client): void { $this->client = $client; } } ================================================ FILE: src/Entities/UserEntityInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Entities; interface UserEntityInterface { /** * Return the user's identifier. * * @return non-empty-string */ public function getIdentifier(): string; } ================================================ FILE: src/EventEmitting/AbstractEvent.php ================================================ name; } /** * Backwards compatibility method * * @deprecated use eventName instead */ public function getName(): string { return $this->name; } public function isPropagationStopped(): bool { return $this->propagationStopped; } public function stopPropagation(): self { $this->propagationStopped = true; return $this; } } ================================================ FILE: src/EventEmitting/EmitterAwareInterface.php ================================================ emitter ??= new EventEmitter(); } public function setEmitter(EventEmitter $emitter): self { $this->emitter = $emitter; return $this; } public function getEventDispatcher(): EventDispatcherInterface { return $this->getEmitter(); } public function getListenerRegistry(): ListenerRegistry { return $this->getEmitter(); } } ================================================ FILE: src/EventEmitting/EventEmitter.php ================================================ subscribeTo($event, $listener, $priority); return $this; } public function emit(object $event): object { return $this->dispatch($event); } } ================================================ FILE: src/Exception/OAuthServerException.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Exception; use Exception; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Throwable; use function htmlspecialchars; use function http_build_query; use function sprintf; class OAuthServerException extends Exception { /** * @var array */ private array $payload; private ServerRequestInterface $serverRequest; /** * Throw a new exception. */ final public function __construct(string $message, int $code, private string $errorType, private int $httpStatusCode = 400, private ?string $hint = null, private ?string $redirectUri = null, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->payload = [ 'error' => $errorType, 'error_description' => $message, ]; if ($hint !== null) { $this->payload['hint'] = $hint; } } /** * Returns the current payload. * * @return array */ public function getPayload(): array { return $this->payload; } /** * Updates the current payload. * * @param array $payload */ public function setPayload(array $payload): void { $this->payload = $payload; } /** * Set the server request that is responsible for generating the exception */ public function setServerRequest(ServerRequestInterface $serverRequest): void { $this->serverRequest = $serverRequest; } /** * Unsupported grant type error. */ public static function unsupportedGrantType(): static { $errorMessage = 'The authorization grant type is not supported by the authorization server.'; $hint = 'Check that all required parameters have been provided'; return new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint); } /** * Invalid request error. */ public static function invalidRequest(string $parameter, ?string $hint = null, ?Throwable $previous = null): static { $errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' . 'includes a parameter more than once, or is otherwise malformed.'; $hint = ($hint === null) ? sprintf('Check the `%s` parameter', $parameter) : $hint; return new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous); } /** * Invalid client error. */ public static function invalidClient(ServerRequestInterface $serverRequest): static { $exception = new static('Client authentication failed', 4, 'invalid_client', 401); $exception->setServerRequest($serverRequest); return $exception; } /** * Invalid scope error */ public static function invalidScope(string $scope, string|null $redirectUri = null): static { $errorMessage = 'The requested scope is invalid, unknown, or malformed'; if ($scope === '') { $hint = 'Specify a scope in the request or set a default scope'; } else { $hint = sprintf( 'Check the `%s` scope', htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false) ); } return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri); } /** * Invalid credentials error. */ public static function invalidCredentials(): static { return new static('The user credentials were incorrect.', 6, 'invalid_grant', 400); } /** * Server error. * * @codeCoverageIgnore */ public static function serverError(string $hint, ?Throwable $previous = null): static { return new static( 'The authorization server encountered an unexpected condition which prevented it from fulfilling' . ' the request: ' . $hint, 7, 'server_error', 500, null, null, $previous ); } /** * Invalid refresh token. */ public static function invalidRefreshToken(?string $hint = null, ?Throwable $previous = null): static { return new static('The refresh token is invalid.', 8, 'invalid_grant', 400, $hint, null, $previous); } /** * Access denied. */ public static function accessDenied(?string $hint = null, ?string $redirectUri = null, ?Throwable $previous = null): static { return new static( 'The resource owner or authorization server denied the request.', 9, 'access_denied', 401, $hint, $redirectUri, $previous ); } /** * Invalid grant. */ public static function invalidGrant(string $hint = ''): static { return new static( 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token ' . 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, ' . 'or was issued to another client.', 10, 'invalid_grant', 400, $hint ); } public function getErrorType(): string { return $this->errorType; } /** * Expired token error. * * @param Throwable $previous Previous exception * * @return static */ public static function expiredToken(?string $hint = null, ?Throwable $previous = null): static { $errorMessage = 'The `device_code` has expired and the device ' . 'authorization session has concluded.'; return new static($errorMessage, 11, 'expired_token', 400, $hint, null, $previous); } public static function authorizationPending(string $hint = '', ?Throwable $previous = null): static { return new static( 'The authorization request is still pending as the end user ' . 'hasn\'t yet completed the user interaction steps. The client ' . 'SHOULD repeat the Access Token Request to the token endpoint', 12, 'authorization_pending', 400, $hint, null, $previous ); } /** * Slow down error used with the Device Authorization Grant. * * * @return static */ public static function slowDown(string $hint = '', ?Throwable $previous = null): static { return new static( 'The authorization request is still pending and polling should ' . 'continue, but the interval MUST be increased ' . 'by 5 seconds for this and all subsequent requests.', 13, 'slow_down', 400, $hint, null, $previous ); } /** * Unauthorized client error. */ public static function unauthorizedClient(?string $hint = null): static { return new static( 'The authenticated client is not authorized to use this authorization grant type.', 14, 'unauthorized_client', 400, $hint ); } /** * Generate a HTTP response. */ public function generateHttpResponse(ResponseInterface $response, bool $useFragment = false, int $jsonOptions = 0): ResponseInterface { $headers = $this->getHttpHeaders(); $payload = $this->getPayload(); if ($this->redirectUri !== null) { if ($useFragment === true) { $this->redirectUri .= (str_contains($this->redirectUri, '#') === false) ? '#' : '&'; } else { $this->redirectUri .= (str_contains($this->redirectUri, '?') === false) ? '?' : '&'; } return $response->withStatus(302)->withHeader('Location', $this->redirectUri . http_build_query($payload)); } foreach ($headers as $header => $content) { $response = $response->withHeader($header, $content); } $jsonEncodedPayload = json_encode($payload, $jsonOptions); $responseBody = $jsonEncodedPayload === false ? 'JSON encoding of payload failed' : $jsonEncodedPayload; $response->getBody()->write($responseBody); return $response->withStatus($this->getHttpStatusCode()); } /** * Get all headers that have to be send with the error response. * * @return array Array with header values */ public function getHttpHeaders(): array { $headers = [ 'Content-type' => 'application/json', ]; // Add "WWW-Authenticate" header // // RFC 6749, section 5.2.: // "If the client attempted to authenticate via the 'Authorization' // request header field, the authorization server MUST // respond with an HTTP 401 (Unauthorized) status code and // include the "WWW-Authenticate" response header field // matching the authentication scheme used by the client. if ($this->errorType === 'invalid_client' && $this->requestHasAuthorizationHeader()) { $authScheme = str_starts_with($this->serverRequest->getHeader('Authorization')[0], 'Bearer') ? 'Bearer' : 'Basic'; $headers['WWW-Authenticate'] = $authScheme . ' realm="OAuth"'; } return $headers; } /** * Check if the exception has an associated redirect URI. * * Returns whether the exception includes a redirect, since * getHttpStatusCode() doesn't return a 302 when there's a * redirect enabled. This helps when you want to override local * error pages but want to let redirects through. */ public function hasRedirect(): bool { return $this->redirectUri !== null; } /** * Returns the Redirect URI used for redirecting. */ public function getRedirectUri(): ?string { return $this->redirectUri; } /** * Returns the HTTP status code to send when the exceptions is output. */ public function getHttpStatusCode(): int { return $this->httpStatusCode; } public function getHint(): ?string { return $this->hint; } /** * Check if the request has a non-empty 'Authorization' header value. * * Returns true if the header is present and not an empty string, false * otherwise. */ private function requestHasAuthorizationHeader(): bool { if (!$this->serverRequest->hasHeader('Authorization')) { return false; } $authorizationHeader = $this->serverRequest->getHeader('Authorization'); // Common .htaccess configurations yield an empty string for the // 'Authorization' header when one is not provided by the client. // For practical purposes that case should be treated as though the // header isn't present. // See https://github.com/thephpleague/oauth2-server/issues/1162 if ($authorizationHeader === [] || $authorizationHeader[0] === '') { return false; } return true; } } ================================================ FILE: src/Exception/UniqueTokenIdentifierConstraintViolationException.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Exception; class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException { public static function create(): UniqueTokenIdentifierConstraintViolationException { $errorMessage = 'Could not create unique access token identifier'; return new static($errorMessage, 100, 'access_token_duplicate', 500); } } ================================================ FILE: src/Grant/AbstractAuthorizeGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; use function http_build_query; abstract class AbstractAuthorizeGrant extends AbstractGrant { /** * @param array $params */ public function makeRedirectUri(string $uri, array $params = [], string $queryDelimiter = '?'): string { $uri .= str_contains($uri, $queryDelimiter) ? '&' : $queryDelimiter; return $uri . http_build_query($params); } protected function createAuthorizationRequest(): AuthorizationRequestInterface { return new AuthorizationRequest(); } /** * Get the client redirect URI. */ protected function getClientRedirectUri(ClientEntityInterface $client): string { return is_array($client->getRedirectUri()) ? $client->getRedirectUri()[0] : $client->getRedirectUri(); } } ================================================ FILE: src/Grant/AbstractGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use DateTimeImmutable; use DomainException; use Error; use Exception; use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use LogicException; use Psr\Http\Message\ServerRequestInterface; use TypeError; use function array_filter; use function array_key_exists; use function base64_decode; use function bin2hex; use function explode; use function is_string; use function random_bytes; use function substr; use function trim; /** * Abstract grant class. */ abstract class AbstractGrant implements GrantTypeInterface { use EmitterAwarePolyfill; use CryptTrait; protected const SCOPE_DELIMITER_STRING = ' '; protected const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10; protected ClientRepositoryInterface $clientRepository; protected AccessTokenRepositoryInterface $accessTokenRepository; protected ScopeRepositoryInterface $scopeRepository; protected AuthCodeRepositoryInterface $authCodeRepository; protected RefreshTokenRepositoryInterface $refreshTokenRepository; protected UserRepositoryInterface $userRepository; protected DateInterval $refreshTokenTTL; protected CryptKeyInterface $privateKey; protected string $defaultScope; protected bool $revokeRefreshTokens = true; public function setClientRepository(ClientRepositoryInterface $clientRepository): void { $this->clientRepository = $clientRepository; } public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void { $this->accessTokenRepository = $accessTokenRepository; } public function setScopeRepository(ScopeRepositoryInterface $scopeRepository): void { $this->scopeRepository = $scopeRepository; } public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void { $this->refreshTokenRepository = $refreshTokenRepository; } public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository): void { $this->authCodeRepository = $authCodeRepository; } public function setUserRepository(UserRepositoryInterface $userRepository): void { $this->userRepository = $userRepository; } /** * {@inheritdoc} */ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL): void { $this->refreshTokenTTL = $refreshTokenTTL; } /** * Set the private key */ public function setPrivateKey(CryptKeyInterface $privateKey): void { $this->privateKey = $privateKey; } public function setDefaultScope(string $scope): void { $this->defaultScope = $scope; } public function revokeRefreshTokens(bool $willRevoke): void { $this->revokeRefreshTokens = $willRevoke; } /** * Validate the client. * * @throws OAuthServerException */ protected function validateClient(ServerRequestInterface $request): ClientEntityInterface { [$clientId, $clientSecret] = $this->getClientCredentials($request); $client = $this->getClientEntityOrFail($clientId, $request); if ($client->isConfidential()) { if ($clientSecret === '') { throw OAuthServerException::invalidRequest('client_secret'); } if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } } return $client; } /** * Wrapper around ClientRepository::getClientEntity() that ensures we emit * an event and throw an exception if the repo doesn't return a client * entity. * * This is a bit of defensive coding because the interface contract * doesn't actually enforce non-null returns/exception-on-no-client so * getClientEntity might return null. By contrast, this method will * always either return a ClientEntityInterface or throw. * * @throws OAuthServerException */ protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface { $client = $this->clientRepository->getClientEntity($clientId); if ($client instanceof ClientEntityInterface === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } if ($this->supportsGrantType($client, $this->getIdentifier()) === false) { throw OAuthServerException::unauthorizedClient(); } return $client; } /** * Returns true if the given client is authorized to use the given grant type. */ protected function supportsGrantType(ClientEntityInterface $client, string $grantType): bool { return method_exists($client, 'supportsGrantType') === false || $client->supportsGrantType($grantType) === true; } /** * Gets the client credentials from the request from the request body or * the Http Basic Authorization header * * @return array{0:non-empty-string,1:string} * * @throws OAuthServerException */ protected function getClientCredentials(ServerRequestInterface $request): array { [$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request); $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser); if ($clientId === null) { throw OAuthServerException::invalidRequest('client_id'); } $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword); return [$clientId, $clientSecret ?? '']; } /** * Validate redirectUri from the request. If a redirect URI is provided * ensure it matches what is pre-registered * * @throws OAuthServerException */ protected function validateRedirectUri( string $redirectUri, ClientEntityInterface $client, ServerRequestInterface $request ): void { $validator = new RedirectUriValidator($client->getRedirectUri()); if (!$validator->validateRedirectUri($redirectUri)) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } } /** * Validate scopes in the request. * * @param null|string|string[] $scopes * * @throws OAuthServerException * * @return ScopeEntityInterface[] */ public function validateScopes(string|array|null $scopes, ?string $redirectUri = null): array { if ($scopes === null) { $scopes = []; } elseif (is_string($scopes)) { $scopes = $this->convertScopesQueryStringToArray($scopes); } $validScopes = []; foreach ($scopes as $scopeItem) { $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem); if ($scope instanceof ScopeEntityInterface === false) { throw OAuthServerException::invalidScope($scopeItem, $redirectUri); } $validScopes[] = $scope; } return $validScopes; } /** * Converts a scopes query string to an array to easily iterate for validation. * * @return string[] */ private function convertScopesQueryStringToArray(string $scopes): array { return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), static fn ($scope) => $scope !== ''); } /** * Parse request parameter. * * @param array $request * * @return non-empty-string|null * * @throws OAuthServerException */ private static function parseParam(string $parameter, array $request, ?string $default = null): ?string { $value = $request[$parameter] ?? ''; if (is_scalar($value)) { $value = trim((string) $value); } else { throw OAuthServerException::invalidRequest($parameter); } if ($value === '') { $value = $default === null ? null : trim($default); if ($value === '') { $value = null; } } return $value; } /** * Retrieve request parameter. * * @return non-empty-string|null * * @throws OAuthServerException */ protected function getRequestParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string { return self::parseParam($parameter, (array) $request->getParsedBody(), $default); } /** * Retrieve HTTP Basic Auth credentials with the Authorization header * of a request. First index of the returned array is the username, * second is the password (so list() will work). If the header does * not exist, or is otherwise an invalid HTTP Basic header, return * [null, null]. * * @return array{0:non-empty-string,1:string}|array{0:null,1:null} */ protected function getBasicAuthCredentials(ServerRequestInterface $request): array { if (!$request->hasHeader('Authorization')) { return [null, null]; } $header = $request->getHeader('Authorization')[0]; if (stripos($header, 'Basic ') !== 0) { return [null, null]; } $decoded = base64_decode(substr($header, 6), true); if ($decoded === false) { return [null, null]; } if (str_contains($decoded, ':') === false) { return [null, null]; // HTTP Basic header without colon isn't valid } [$username, $password] = explode(':', $decoded, 2); if ($username === '') { return [null, null]; } return [$username, $password]; } /** * Retrieve query string parameter. * * @return non-empty-string|null * * @throws OAuthServerException */ protected function getQueryStringParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string { return self::parseParam($parameter, $request->getQueryParams(), $default); } /** * Retrieve cookie parameter. * * @return non-empty-string|null * * @throws OAuthServerException */ protected function getCookieParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string { return self::parseParam($parameter, $request->getCookieParams(), $default); } /** * Retrieve server parameter. * * @return non-empty-string|null * * @throws OAuthServerException */ protected function getServerParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string { return self::parseParam($parameter, $request->getServerParams(), $default); } /** * Issue an access token. * * @param ScopeEntityInterface[] $scopes * * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException */ protected function issueAccessToken( DateInterval $accessTokenTTL, ClientEntityInterface $client, string|null $userIdentifier, array $scopes = [] ): AccessTokenEntityInterface { $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL)); $accessToken->setPrivateKey($this->privateKey); while ($maxGenerationAttempts-- > 0) { $accessToken->setIdentifier($this->generateUniqueIdentifier()); try { $this->accessTokenRepository->persistNewAccessToken($accessToken); return $accessToken; } catch (UniqueTokenIdentifierConstraintViolationException $e) { if ($maxGenerationAttempts === 0) { throw $e; } } } // This should never be hit. It is here to work around a PHPStan false error return $accessToken; } /** * Issue an auth code. * * @param non-empty-string $userIdentifier * @param ScopeEntityInterface[] $scopes * * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException */ protected function issueAuthCode( DateInterval $authCodeTTL, ClientEntityInterface $client, string $userIdentifier, ?string $redirectUri, array $scopes = [] ): AuthCodeEntityInterface { $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; $authCode = $this->authCodeRepository->getNewAuthCode(); $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL)); $authCode->setClient($client); $authCode->setUserIdentifier($userIdentifier); if ($redirectUri !== null) { $authCode->setRedirectUri($redirectUri); } foreach ($scopes as $scope) { $authCode->addScope($scope); } while ($maxGenerationAttempts-- > 0) { $authCode->setIdentifier($this->generateUniqueIdentifier()); try { $this->authCodeRepository->persistNewAuthCode($authCode); return $authCode; } catch (UniqueTokenIdentifierConstraintViolationException $e) { if ($maxGenerationAttempts === 0) { throw $e; } } } // This should never be hit. It is here to work around a PHPStan false error return $authCode; } /** * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException */ protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface { if ($this->supportsGrantType($accessToken->getClient(), 'refresh_token') === false) { return null; } $refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); if ($refreshToken === null) { return null; } $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL)); $refreshToken->setAccessToken($accessToken); $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; while ($maxGenerationAttempts-- > 0) { $refreshToken->setIdentifier($this->generateUniqueIdentifier()); try { $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); return $refreshToken; } catch (UniqueTokenIdentifierConstraintViolationException $e) { if ($maxGenerationAttempts === 0) { throw $e; } } } // This should never be hit. It is here to work around a PHPStan false error return $refreshToken; } /** * Generate a new unique identifier. * * @return non-empty-string * * @throws OAuthServerException */ protected function generateUniqueIdentifier(int $length = 40): string { try { if ($length < 1) { throw new DomainException('Length must be a positive integer'); } return bin2hex(random_bytes($length)); // @codeCoverageIgnoreStart } catch (TypeError | Error $e) { throw OAuthServerException::serverError('An unexpected error has occurred', $e); } catch (Exception $e) { // If you get this message, the CSPRNG failed hard. throw OAuthServerException::serverError('Could not generate a random string', $e); } // @codeCoverageIgnoreEnd } /** * {@inheritdoc} */ public function canRespondToAccessTokenRequest(ServerRequestInterface $request): bool { $requestParameters = (array) $request->getParsedBody(); return ( array_key_exists('grant_type', $requestParameters) && $requestParameters['grant_type'] === $this->getIdentifier() ); } /** * {@inheritdoc} */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool { return false; } /** * {@inheritdoc} */ public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface { throw new LogicException('This grant cannot validate an authorization request'); } /** * {@inheritdoc} */ public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface { throw new LogicException('This grant cannot complete an authorization request'); } /** * {@inheritdoc} */ public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request): bool { return false; } /** * {@inheritdoc} */ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse { throw new LogicException('This grant cannot validate a device authorization request'); } /** * {@inheritdoc} */ public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void { throw new LogicException('This grant cannot complete a device authorization request'); } /** * {@inheritdoc} */ public function setIntervalVisibility(bool $intervalVisibility): void { throw new LogicException('This grant does not support the interval parameter'); } /** * {@inheritdoc} */ public function getIntervalVisibility(): bool { return false; } /** * {@inheritdoc} */ public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void { throw new LogicException('This grant does not support the verification_uri_complete parameter'); } } ================================================ FILE: src/Grant/AuthCodeGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use DateTimeImmutable; use Exception; use InvalidArgumentException; use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface; use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier; use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use LogicException; use Psr\Http\Message\ServerRequestInterface; use stdClass; use function array_key_exists; use function array_keys; use function array_map; use function count; use function hash_algos; use function implode; use function in_array; use function is_array; use function json_decode; use function json_encode; use function preg_match; use function property_exists; use function sprintf; use function time; class AuthCodeGrant extends AbstractAuthorizeGrant { private bool $requireCodeChallengeForPublicClients = true; /** * @var CodeChallengeVerifierInterface[] */ private array $codeChallengeVerifiers = []; /** * @throws Exception */ public function __construct( AuthCodeRepositoryInterface $authCodeRepository, RefreshTokenRepositoryInterface $refreshTokenRepository, private DateInterval $authCodeTTL ) { $this->setAuthCodeRepository($authCodeRepository); $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new DateInterval('P1M'); if (in_array('sha256', hash_algos(), true)) { $s256Verifier = new S256Verifier(); $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier; } $plainVerifier = new PlainVerifier(); $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier; } /** * Disable the requirement for a code challenge for public clients. */ public function disableRequireCodeChallengeForPublicClients(): void { $this->requireCodeChallengeForPublicClients = false; } /** * Respond to an access token request. * * @throws OAuthServerException */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { $client = $this->validateClient($request); $encryptedAuthCode = $this->getRequestParameter('code', $request); if ($encryptedAuthCode === null) { throw OAuthServerException::invalidRequest('code'); } try { $authCodePayload = json_decode($this->decrypt($encryptedAuthCode)); $this->validateAuthorizationCode($authCodePayload, $client, $request); $scopes = $this->scopeRepository->finalizeScopes( $this->validateScopes($authCodePayload->scopes), $this->getIdentifier(), $client, $authCodePayload->user_id, $authCodePayload->auth_code_id ); } catch (InvalidArgumentException $e) { throw OAuthServerException::invalidGrant('Cannot validate the provided authorization code'); } catch (LogicException $e) { throw OAuthServerException::invalidRequest('code', 'Issue decrypting the authorization code', $e); } $codeVerifier = $this->getRequestParameter('code_verifier', $request); // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack if (!isset($authCodePayload->code_challenge) && $codeVerifier !== null) { throw OAuthServerException::invalidRequest( 'code_challenge', 'code_verifier received when no code_challenge is present' ); } if (isset($authCodePayload->code_challenge)) { $this->validateCodeChallenge($authCodePayload, $codeVerifier); } // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } // Revoke used auth code $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); return $responseType; } private function validateCodeChallenge(object $authCodePayload, ?string $codeVerifier): void { if ($codeVerifier === null) { throw OAuthServerException::invalidRequest('code_verifier'); } // Validate code_verifier according to RFC-7636 // @see: https://tools.ietf.org/html/rfc7636#section-4.1 if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) { throw OAuthServerException::invalidRequest( 'code_verifier', 'Code Verifier must follow the specifications of RFC-7636.' ); } if (property_exists($authCodePayload, 'code_challenge_method')) { if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) { $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method]; if ( !property_exists($authCodePayload, 'code_challenge') || !isset($authCodePayload->code_challenge) || $codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false ) { throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); } } else { throw OAuthServerException::serverError( sprintf( 'Unsupported code challenge method `%s`', $authCodePayload->code_challenge_method ) ); } } } /** * Validate the authorization code. */ private function validateAuthorizationCode( stdClass $authCodePayload, ClientEntityInterface $client, ServerRequestInterface $request ): void { if (!property_exists($authCodePayload, 'auth_code_id')) { throw OAuthServerException::invalidRequest('code', 'Authorization code malformed'); } if (time() > $authCodePayload->expire_time) { throw OAuthServerException::invalidGrant('Authorization code has expired'); } if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { throw OAuthServerException::invalidGrant('Authorization code has been revoked'); } if ($authCodePayload->client_id !== $client->getIdentifier()) { throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); } // The redirect URI is required in this request if it was specified // in the authorization request $redirectUri = $this->getRequestParameter('redirect_uri', $request); if ($authCodePayload->redirect_uri !== null && $redirectUri === null) { throw OAuthServerException::invalidRequest('redirect_uri'); } // If a redirect URI has been provided ensure it matches the stored redirect URI if ($redirectUri !== null && $authCodePayload->redirect_uri !== $redirectUri) { throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); } } /** * Return the grant identifier that can be used in matching up requests. */ public function getIdentifier(): string { return 'authorization_code'; } /** * {@inheritdoc} */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool { return ( array_key_exists('response_type', $request->getQueryParams()) && $request->getQueryParams()['response_type'] === 'code' && isset($request->getQueryParams()['client_id']) ); } /** * {@inheritdoc} */ public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface { $clientId = $this->getQueryStringParameter( 'client_id', $request, $this->getServerParameter('PHP_AUTH_USER', $request) ); if ($clientId === null) { throw OAuthServerException::invalidRequest('client_id'); } $client = $this->getClientEntityOrFail($clientId, $request); $redirectUri = $this->getQueryStringParameter('redirect_uri', $request); if ($redirectUri !== null) { $this->validateRedirectUri($redirectUri, $client, $request); } elseif ( $client->getRedirectUri() === '' || (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1) ) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } $stateParameter = $this->getQueryStringParameter('state', $request); $scopes = $this->validateScopes( $this->getQueryStringParameter('scope', $request, $this->defaultScope), $this->makeRedirectUri( $redirectUri ?? $this->getClientRedirectUri($client), $stateParameter !== null ? ['state' => $stateParameter] : [] ) ); $authorizationRequest = $this->createAuthorizationRequest(); $authorizationRequest->setGrantTypeId($this->getIdentifier()); $authorizationRequest->setClient($client); $authorizationRequest->setRedirectUri($redirectUri); if ($stateParameter !== null) { $authorizationRequest->setState($stateParameter); } $authorizationRequest->setScopes($scopes); $codeChallenge = $this->getQueryStringParameter('code_challenge', $request); if ($codeChallenge !== null) { $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain'); if ($codeChallengeMethod === null) { throw OAuthServerException::invalidRequest( 'code_challenge_method', 'Code challenge method must be provided when `code_challenge` is set.' ); } if (array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) { throw OAuthServerException::invalidRequest( 'code_challenge_method', 'Code challenge method must be one of ' . implode(', ', array_map( function ($method) { return '`' . $method . '`'; }, array_keys($this->codeChallengeVerifiers) )) ); } // Validate code_challenge according to RFC-7636 // @see: https://tools.ietf.org/html/rfc7636#section-4.2 if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) { throw OAuthServerException::invalidRequest( 'code_challenge', 'Code challenge must follow the specifications of RFC-7636.' ); } $authorizationRequest->setCodeChallenge($codeChallenge); $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod); } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) { throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients'); } return $authorizationRequest; } /** * {@inheritdoc} */ public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface { if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } $finalRedirectUri = $authorizationRequest->getRedirectUri() ?? $this->getClientRedirectUri($authorizationRequest->getClient()); // The user approved the client, redirect them back with an auth code if ($authorizationRequest->isAuthorizationApproved() === true) { $authCode = $this->issueAuthCode( $this->authCodeTTL, $authorizationRequest->getClient(), $authorizationRequest->getUser()->getIdentifier(), $authorizationRequest->getRedirectUri(), $authorizationRequest->getScopes() ); $payload = [ 'client_id' => $authCode->getClient()->getIdentifier(), 'redirect_uri' => $authCode->getRedirectUri(), 'auth_code_id' => $authCode->getIdentifier(), 'scopes' => $authCode->getScopes(), 'user_id' => $authCode->getUserIdentifier(), 'expire_time' => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(), 'code_challenge' => $authorizationRequest->getCodeChallenge(), 'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(), ]; $jsonPayload = json_encode($payload); if ($jsonPayload === false) { throw new LogicException('An error was encountered when JSON encoding the authorization request response'); } $response = new RedirectResponse(); $response->setRedirectUri( $this->makeRedirectUri( $finalRedirectUri, [ 'code' => $this->encrypt($jsonPayload), 'state' => $authorizationRequest->getState(), ] ) ); return $response; } // The user denied the client, redirect them back with an error throw OAuthServerException::accessDenied( 'The user denied the request', $this->makeRedirectUri( $finalRedirectUri, [ 'state' => $authorizationRequest->getState(), ] ) ); } } ================================================ FILE: src/Grant/ClientCredentialsGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; /** * Client credentials grant class. */ class ClientCredentialsGrant extends AbstractGrant { /** * {@inheritdoc} */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { $client = $this->validateClient($request); if (!$client->isConfidential()) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client); // Issue and persist access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes); // Send event to emitter $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); // Inject access token into response type $responseType->setAccessToken($accessToken); return $responseType; } /** * {@inheritdoc} */ public function getIdentifier(): string { return 'client_credentials'; } } ================================================ FILE: src/Grant/DeviceCodeGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use DateTimeImmutable; use Error; use Exception; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; use TypeError; use function is_null; use function random_int; use function strlen; use function time; /** * Device Code grant class. */ class DeviceCodeGrant extends AbstractGrant { protected DeviceCodeRepositoryInterface $deviceCodeRepository; private bool $includeVerificationUriComplete = false; private bool $intervalVisibility = false; private string $verificationUri; public function __construct( DeviceCodeRepositoryInterface $deviceCodeRepository, RefreshTokenRepositoryInterface $refreshTokenRepository, private DateInterval $deviceCodeTTL, string $verificationUri, private readonly int $retryInterval = 5 ) { $this->setDeviceCodeRepository($deviceCodeRepository); $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new DateInterval('P1M'); $this->setVerificationUri($verificationUri); } /** * {@inheritdoc} */ public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request): bool { return true; } /** * {@inheritdoc} */ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse { $clientId = $this->getRequestParameter( 'client_id', $request, $this->getServerParameter('PHP_AUTH_USER', $request) ); if ($clientId === null) { throw OAuthServerException::invalidRequest('client_id'); } $client = $this->getClientEntityOrFail($clientId, $request); $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); $deviceCodeEntity = $this->issueDeviceCode( $this->deviceCodeTTL, $client, $this->verificationUri, $scopes ); $response = new DeviceCodeResponse(); if ($this->includeVerificationUriComplete === true) { $response->includeVerificationUriComplete(); } if ($this->intervalVisibility === true) { $response->includeInterval(); } $response->setDeviceCodeEntity($deviceCodeEntity); return $response; } /** * {@inheritdoc} */ public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void { $deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode($deviceCode); if ($deviceCode instanceof DeviceCodeEntityInterface === false) { throw OAuthServerException::invalidRequest('device_code', 'Device code does not exist'); } if ($userId === '') { throw OAuthServerException::invalidRequest('user_id', 'User ID is required'); } $deviceCode->setUserIdentifier($userId); $deviceCode->setUserApproved($userApproved); $this->deviceCodeRepository->persistDeviceCode($deviceCode); } /** * {@inheritdoc} */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { // Validate request $client = $this->validateClient($request); $deviceCodeEntity = $this->validateDeviceCode($request, $client); // If device code has no user associated, respond with pending or slow down if (is_null($deviceCodeEntity->getUserIdentifier())) { $shouldSlowDown = $this->deviceCodePolledTooSoon($deviceCodeEntity->getLastPolledAt()); $deviceCodeEntity->setLastPolledAt(new DateTimeImmutable()); $this->deviceCodeRepository->persistDeviceCode($deviceCodeEntity); if ($shouldSlowDown) { throw OAuthServerException::slowDown(); } throw OAuthServerException::authorizationPending(); } if ($deviceCodeEntity->getUserApproved() === false) { throw OAuthServerException::accessDenied(); } // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes($deviceCodeEntity->getScopes(), $this->getIdentifier(), $client, $deviceCodeEntity->getUserIdentifier()); // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $deviceCodeEntity->getUserIdentifier(), $finalizedScopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } $this->deviceCodeRepository->revokeDeviceCode($deviceCodeEntity->getIdentifier()); return $responseType; } /** * @throws OAuthServerException */ protected function validateDeviceCode(ServerRequestInterface $request, ClientEntityInterface $client): DeviceCodeEntityInterface { $deviceCode = $this->getRequestParameter('device_code', $request); if (is_null($deviceCode)) { throw OAuthServerException::invalidRequest('device_code'); } $deviceCodeEntity = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode( $deviceCode ); if ($deviceCodeEntity instanceof DeviceCodeEntityInterface === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidGrant(); } if (time() > $deviceCodeEntity->getExpiryDateTime()->getTimestamp()) { throw OAuthServerException::expiredToken('device_code'); } if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCode) === true) { throw OAuthServerException::invalidRequest('device_code', 'Device code has been revoked'); } if ($deviceCodeEntity->getClient()->getIdentifier() !== $client->getIdentifier()) { throw OAuthServerException::invalidRequest('device_code', 'Device code was not issued to this client'); } return $deviceCodeEntity; } private function deviceCodePolledTooSoon(?DateTimeImmutable $lastPoll): bool { return $lastPoll !== null && $lastPoll->getTimestamp() + $this->retryInterval > time(); } /** * Set the verification uri */ public function setVerificationUri(string $verificationUri): void { $this->verificationUri = $verificationUri; } /** * {@inheritdoc} */ public function getIdentifier(): string { return 'urn:ietf:params:oauth:grant-type:device_code'; } private function setDeviceCodeRepository(DeviceCodeRepositoryInterface $deviceCodeRepository): void { $this->deviceCodeRepository = $deviceCodeRepository; } /** * Issue a device code. * * @param ScopeEntityInterface[] $scopes * * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException */ protected function issueDeviceCode( DateInterval $deviceCodeTTL, ClientEntityInterface $client, string $verificationUri, array $scopes = [], ): DeviceCodeEntityInterface { $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; $deviceCode = $this->deviceCodeRepository->getNewDeviceCode(); $deviceCode->setExpiryDateTime((new DateTimeImmutable())->add($deviceCodeTTL)); $deviceCode->setClient($client); $deviceCode->setVerificationUri($verificationUri); $deviceCode->setInterval($this->retryInterval); foreach ($scopes as $scope) { $deviceCode->addScope($scope); } while ($maxGenerationAttempts-- > 0) { $deviceCode->setIdentifier($this->generateUniqueIdentifier()); $deviceCode->setUserCode($this->generateUserCode()); try { $this->deviceCodeRepository->persistDeviceCode($deviceCode); return $deviceCode; } catch (UniqueTokenIdentifierConstraintViolationException $e) { if ($maxGenerationAttempts === 0) { throw $e; } } } // This should never be hit. It is here to work around a PHPStan false error return $deviceCode; } /** * Generate a new user code. * * @throws OAuthServerException */ protected function generateUserCode(int $length = 8): string { try { $userCode = ''; $userCodeCharacters = 'BCDFGHJKLMNPQRSTVWXZ'; while (strlen($userCode) < $length) { $userCode .= $userCodeCharacters[random_int(0, 19)]; } return $userCode; // @codeCoverageIgnoreStart } catch (TypeError | Error $e) { throw OAuthServerException::serverError('An unexpected error has occurred', $e); } catch (Exception $e) { // If you get this message, the CSPRNG failed hard. throw OAuthServerException::serverError('Could not generate a random string', $e); } // @codeCoverageIgnoreEnd } public function setIntervalVisibility(bool $intervalVisibility): void { $this->intervalVisibility = $intervalVisibility; } public function getIntervalVisibility(): bool { return $this->intervalVisibility; } public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void { $this->includeVerificationUriComplete = $includeVerificationUriComplete; } } ================================================ FILE: src/Grant/GrantTypeInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use Defuse\Crypto\Key; use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\EventEmitting\EmitterAwareInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; /** * Grant type interface. */ interface GrantTypeInterface extends EmitterAwareInterface { /** * Set refresh token TTL. */ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL): void; /** * Return the grant identifier that can be used in matching up requests. */ public function getIdentifier(): string; /** * Respond to an incoming request. */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface; /** * The grant type should return true if it is able to respond to an authorization request */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool; /** * If the grant can respond to an authorization request this method should be called to validate the parameters of * the request. * * If the validation is successful an AuthorizationRequest object will be returned. This object can be safely * serialized in a user's session, and can be used during user authentication and authorization. */ public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface; /** * Once a user has authenticated and authorized the client the grant can complete the authorization request. * The AuthorizationRequest object's $userId property must be set to the authenticated user and the * $authorizationApproved property must reflect their desire to authorize or deny the client. */ public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface; /** * The grant type should return true if it is able to respond to this request. * * For example most grant types will check that the $_POST['grant_type'] property matches it's identifier property. */ public function canRespondToAccessTokenRequest(ServerRequestInterface $request): bool; /** * The grant type should return true if it is able to respond to a device authorization request */ public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request): bool; /** * If the grant can respond to a device authorization request this method should be called to validate the parameters of * the request. * * If the validation is successful a DeviceAuthorizationRequest object will be returned. This object can be safely * serialized in a user's session, and can be used during user authentication and authorization. */ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse; /** * If the grant can respond to a device authorization request this method should be called to validate the parameters of * the request. * * If the validation is successful a DeviceCode object is persisted. */ public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void; /** * Set the client repository. */ public function setClientRepository(ClientRepositoryInterface $clientRepository): void; /** * Set the access token repository. */ public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void; /** * Set the scope repository. */ public function setScopeRepository(ScopeRepositoryInterface $scopeRepository): void; /** * Set the default scope. */ public function setDefaultScope(string $scope): void; /** * Set the path to the private key. */ public function setPrivateKey(CryptKeyInterface $privateKey): void; public function setEncryptionKey(Key|string|null $key = null): void; /** * Enable or prevent the revocation of refresh tokens upon usage. */ public function revokeRefreshTokens(bool $willRevoke): void; /** * If set, the minimum interval between device code polling will be * returned by the server. */ public function setIntervalVisibility(bool $intervalVisibility): void; /** * Checks if the minimum interval between device code polling should be * returned by the server. */ public function getIntervalVisibility(): bool; /** * If set, the server will return a full verification URI to the client. * This is useful when your device authorization endpoint might not be able * to enter the user code easily. */ public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void; } ================================================ FILE: src/Grant/ImplicitGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use LogicException; use Psr\Http\Message\ServerRequestInterface; use function count; use function is_array; use function is_null; use function time; class ImplicitGrant extends AbstractAuthorizeGrant { public function __construct(private DateInterval $accessTokenTTL, private string $queryDelimiter = '#') { } /** * @throws LogicException */ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL): void { throw new LogicException('The Implicit Grant does not return refresh tokens'); } /** * @throws LogicException */ public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void { throw new LogicException('The Implicit Grant does not return refresh tokens'); } /** * {@inheritdoc} */ public function canRespondToAccessTokenRequest(ServerRequestInterface $request): bool { return false; } /** * Return the grant identifier that can be used in matching up requests. */ public function getIdentifier(): string { return 'implicit'; } /** * Respond to an incoming request. */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { throw new LogicException('This grant does not used this method'); } /** * {@inheritdoc} */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool { return ( isset($request->getQueryParams()['response_type']) && $request->getQueryParams()['response_type'] === 'token' && isset($request->getQueryParams()['client_id']) ); } /** * {@inheritdoc} */ public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface { $clientId = $this->getQueryStringParameter( 'client_id', $request, $this->getServerParameter('PHP_AUTH_USER', $request) ); if (is_null($clientId)) { throw OAuthServerException::invalidRequest('client_id'); } $client = $this->getClientEntityOrFail($clientId, $request); $redirectUri = $this->getQueryStringParameter('redirect_uri', $request); if ($redirectUri !== null) { $this->validateRedirectUri($redirectUri, $client, $request); } elseif ( $client->getRedirectUri() === '' || (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1) ) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } $stateParameter = $this->getQueryStringParameter('state', $request); $scopes = $this->validateScopes( $this->getQueryStringParameter('scope', $request, $this->defaultScope), $this->makeRedirectUri( $redirectUri ?? $this->getClientRedirectUri($client), $stateParameter !== null ? ['state' => $stateParameter] : [], $this->queryDelimiter ) ); $authorizationRequest = $this->createAuthorizationRequest(); $authorizationRequest->setGrantTypeId($this->getIdentifier()); $authorizationRequest->setClient($client); $authorizationRequest->setRedirectUri($redirectUri); if ($stateParameter !== null) { $authorizationRequest->setState($stateParameter); } $authorizationRequest->setScopes($scopes); return $authorizationRequest; } /** * {@inheritdoc} */ public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface { if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } $finalRedirectUri = $authorizationRequest->getRedirectUri() ?? $this->getClientRedirectUri($authorizationRequest->getClient()); // The user approved the client, redirect them back with an access token if ($authorizationRequest->isAuthorizationApproved() === true) { // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes( $authorizationRequest->getScopes(), $this->getIdentifier(), $authorizationRequest->getClient(), $authorizationRequest->getUser()->getIdentifier() ); $accessToken = $this->issueAccessToken( $this->accessTokenTTL, $authorizationRequest->getClient(), $authorizationRequest->getUser()->getIdentifier(), $finalizedScopes ); // TODO: next major release: this method needs `ServerRequestInterface` as an argument // $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $response = new RedirectResponse(); $response->setRedirectUri( $this->makeRedirectUri( $finalRedirectUri, [ 'access_token' => $accessToken->toString(), 'token_type' => 'Bearer', 'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - time(), 'state' => $authorizationRequest->getState(), ], $this->queryDelimiter ) ); return $response; } // The user denied the client, redirect them back with an error throw OAuthServerException::accessDenied( 'The user denied the request', $this->makeRedirectUri( $finalRedirectUri, [ 'state' => $authorizationRequest->getState(), ], $this->queryDelimiter ) ); } } ================================================ FILE: src/Grant/PasswordGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; /** * Password grant class. */ class PasswordGrant extends AbstractGrant { public function __construct( UserRepositoryInterface $userRepository, RefreshTokenRepositoryInterface $refreshTokenRepository ) { $this->setUserRepository($userRepository); $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new DateInterval('P1M'); } /** * {@inheritdoc} */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { // Validate request $client = $this->validateClient($request); $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); $user = $this->validateUser($request, $client); $finalizedScopes = $this->scopeRepository->finalizeScopes( $scopes, $this->getIdentifier(), $client, $user->getIdentifier() ); // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } return $responseType; } /** * @throws OAuthServerException */ protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client): UserEntityInterface { $username = $this->getRequestParameter('username', $request) ?? throw OAuthServerException::invalidRequest('username'); $password = $this->getRequestParameter('password', $request) ?? throw OAuthServerException::invalidRequest('password'); $user = $this->userRepository->getUserEntityByUserCredentials( $username, $password, $this->getIdentifier(), $client ); if ($user instanceof UserEntityInterface === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidCredentials(); } return $user; } /** * {@inheritdoc} */ public function getIdentifier(): string { return 'password'; } } ================================================ FILE: src/Grant/RefreshTokenGrant.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Grant; use DateInterval; use Exception; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; use function implode; use function in_array; use function json_decode; use function time; /** * Refresh token grant. */ class RefreshTokenGrant extends AbstractGrant { public function __construct(RefreshTokenRepositoryInterface $refreshTokenRepository) { $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new DateInterval('P1M'); } /** * {@inheritdoc} */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { // Validate request $client = $this->validateClient($request); $oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier()); $scopes = $this->validateScopes( $this->getRequestParameter( 'scope', $request, implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes']) ) ); // The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure // the request doesn't include any new scopes foreach ($scopes as $scope) { if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) { throw OAuthServerException::invalidScope($scope->getIdentifier()); } } $userId = $oldRefreshToken['user_id']; if (is_int($userId)) { $userId = (string) $userId; } $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $userId); // Expire old tokens $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); if ($this->revokeRefreshTokens) { $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); } // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $userId, $scopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } return $responseType; } /** * @throws OAuthServerException * * @return array */ protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): array { $encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request) ?? throw OAuthServerException::invalidRequest('refresh_token'); // Validate refresh token try { $refreshToken = $this->decrypt($encryptedRefreshToken); } catch (Exception $e) { throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); } $refreshTokenData = json_decode($refreshToken, true); if ($refreshTokenData['client_id'] !== $clientId) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); } if ($refreshTokenData['expire_time'] < time()) { throw OAuthServerException::invalidRefreshToken('Token has expired'); } if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { throw OAuthServerException::invalidRefreshToken('Token has been revoked'); } return $refreshTokenData; } /** * {@inheritdoc} */ public function getIdentifier(): string { return 'refresh_token'; } } ================================================ FILE: src/Middleware/AuthorizationServerMiddleware.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Middleware; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class AuthorizationServerMiddleware { public function __construct(private AuthorizationServer $server) { } public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface { try { $response = $this->server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } // Pass the request and response on to the next responder in the chain return $next($request, $response); } } ================================================ FILE: src/Middleware/ResourceServerMiddleware.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Middleware; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\ResourceServer; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class ResourceServerMiddleware { public function __construct(private ResourceServer $server) { } public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface { try { $request = $this->server->validateAuthenticatedRequest($request); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } // Pass the request and response on to the next responder in the chain return $next($request, $response); } } ================================================ FILE: src/RedirectUriValidators/RedirectUriValidator.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\RedirectUriValidators; use League\Uri\Exceptions\SyntaxError; use League\Uri\Uri; use function in_array; use function is_string; class RedirectUriValidator implements RedirectUriValidatorInterface { /** * @var string[] */ private array $allowedRedirectUris; /** * New validator instance for the given uri * * @param string[]|string $allowedRedirectUris */ public function __construct(array|string $allowedRedirectUris) { if (is_string($allowedRedirectUris)) { $this->allowedRedirectUris = [$allowedRedirectUris]; } else { $this->allowedRedirectUris = $allowedRedirectUris; } } /** * Validates the redirect uri. * * @return bool Return true if valid, false otherwise */ public function validateRedirectUri(string $redirectUri): bool { if ($this->isLoopbackUri($redirectUri)) { return $this->matchUriExcludingPort($redirectUri); } return $this->matchExactUri($redirectUri); } /** * According to section 7.3 of rfc8252, loopback uris are: * - "http://127.0.0.1:{port}/{path}" for IPv4 * - "http://[::1]:{port}/{path}" for IPv6 */ private function isLoopbackUri(string $redirectUri): bool { try { $uri = Uri::new($redirectUri); } catch (SyntaxError $e) { return false; } return $uri->getScheme() === 'http' && (in_array($uri->getHost(), ['127.0.0.1', '[::1]'], true)); } /** * Find an exact match among allowed uris */ private function matchExactUri(string $redirectUri): bool { return in_array($redirectUri, $this->allowedRedirectUris, true); } /** * Find a match among allowed uris, allowing for different port numbers */ private function matchUriExcludingPort(string $redirectUri): bool { $parsedUrl = $this->parseUrlAndRemovePort($redirectUri); foreach ($this->allowedRedirectUris as $allowedRedirectUri) { if ($parsedUrl === $this->parseUrlAndRemovePort($allowedRedirectUri)) { return true; } } return false; } /** * Parse an url like \parse_url, excluding the port */ private function parseUrlAndRemovePort(string $url): string { $uri = Uri::new($url); return (string) $uri->withPort(null); } } ================================================ FILE: src/RedirectUriValidators/RedirectUriValidatorInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\RedirectUriValidators; interface RedirectUriValidatorInterface { /** * Validates the redirect uri. */ public function validateRedirectUri(string $redirectUri): bool; } ================================================ FILE: src/Repositories/AccessTokenRepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; /** * Access token interface. */ interface AccessTokenRepositoryInterface extends RepositoryInterface { /** * Create a new access token * * @param ScopeEntityInterface[] $scopes */ public function getNewToken( ClientEntityInterface $clientEntity, array $scopes, string|null $userIdentifier = null ): AccessTokenEntityInterface; /** * @throws UniqueTokenIdentifierConstraintViolationException */ public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void; public function revokeAccessToken(string $tokenId): void; public function isAccessTokenRevoked(string $tokenId): bool; } ================================================ FILE: src/Repositories/AuthCodeRepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; /** * Auth code storage interface. */ interface AuthCodeRepositoryInterface extends RepositoryInterface { public function getNewAuthCode(): AuthCodeEntityInterface; /** * @throws UniqueTokenIdentifierConstraintViolationException */ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void; public function revokeAuthCode(string $codeId): void; public function isAuthCodeRevoked(string $codeId): bool; } ================================================ FILE: src/Repositories/ClientRepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; /** * Client storage interface. */ interface ClientRepositoryInterface extends RepositoryInterface { /** * Get a client. */ public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface; /** * Validate a client's secret. */ public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool; } ================================================ FILE: src/Repositories/DeviceCodeRepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; interface DeviceCodeRepositoryInterface extends RepositoryInterface { /** * Creates a new DeviceCode */ public function getNewDeviceCode(): DeviceCodeEntityInterface; /** * Persists a device code to permanent storage. * * @throws UniqueTokenIdentifierConstraintViolationException */ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): void; /** * Get a device code entity. */ public function getDeviceCodeEntityByDeviceCode( string $deviceCodeEntity // TODO: next major release: rename to `$deviceCode` ): ?DeviceCodeEntityInterface; /** * Revoke a device code. */ public function revokeDeviceCode(string $codeId): void; /** * Check if the device code has been revoked. * * @return bool Return true if this code has been revoked */ public function isDeviceCodeRevoked(string $codeId): bool; } ================================================ FILE: src/Repositories/RefreshTokenRepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; /** * Refresh token interface. */ interface RefreshTokenRepositoryInterface extends RepositoryInterface { public function getNewRefreshToken(): ?RefreshTokenEntityInterface; /** * @throws UniqueTokenIdentifierConstraintViolationException */ public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void; public function revokeRefreshToken(string $tokenId): void; public function isRefreshTokenRevoked(string $tokenId): bool; } ================================================ FILE: src/Repositories/RepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; /** * Repository interface. */ interface RepositoryInterface { } ================================================ FILE: src/Repositories/ScopeRepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; /** * Scope interface. */ interface ScopeRepositoryInterface extends RepositoryInterface { /** * Return information about a scope. * * @param string $identifier The scope identifier */ public function getScopeEntityByIdentifier(string $identifier): ?ScopeEntityInterface; /** * Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally * append additional scopes or remove requested scopes. * * @param ScopeEntityInterface[] $scopes * * @return ScopeEntityInterface[] */ public function finalizeScopes( array $scopes, string $grantType, ClientEntityInterface $clientEntity, string|null $userIdentifier = null, ?string $authCodeId = null ): array; } ================================================ FILE: src/Repositories/UserRepositoryInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; interface UserRepositoryInterface extends RepositoryInterface { /** * Get a user entity. */ public function getUserEntityByUserCredentials( string $username, string $password, string $grantType, ClientEntityInterface $clientEntity ): ?UserEntityInterface; } ================================================ FILE: src/RequestAccessTokenEvent.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use Psr\Http\Message\ServerRequestInterface; use SensitiveParameter; class RequestAccessTokenEvent extends RequestEvent { public function __construct( string $name, ServerRequestInterface $request, #[SensitiveParameter] private AccessTokenEntityInterface $accessToken ) { parent::__construct($name, $request); } /** * @codeCoverageIgnore */ public function getAccessToken(): AccessTokenEntityInterface { return $this->accessToken; } } ================================================ FILE: src/RequestEvent.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server; use League\OAuth2\Server\EventEmitting\AbstractEvent; use Psr\Http\Message\ServerRequestInterface; class RequestEvent extends AbstractEvent { public const CLIENT_AUTHENTICATION_FAILED = 'client.authentication.failed'; public const USER_AUTHENTICATION_FAILED = 'user.authentication.failed'; public const REFRESH_TOKEN_CLIENT_FAILED = 'refresh_token.client.failed'; public const REFRESH_TOKEN_ISSUED = 'refresh_token.issued'; public const ACCESS_TOKEN_ISSUED = 'access_token.issued'; public function __construct(string $name, private ServerRequestInterface $request) { parent::__construct($name); } /** * @codeCoverageIgnore */ public function getRequest(): ServerRequestInterface { return $this->request; } } ================================================ FILE: src/RequestRefreshTokenEvent.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use Psr\Http\Message\ServerRequestInterface; use SensitiveParameter; class RequestRefreshTokenEvent extends RequestEvent { public function __construct( string $name, ServerRequestInterface $request, #[SensitiveParameter] private RefreshTokenEntityInterface $refreshToken ) { parent::__construct($name, $request); } /** * @codeCoverageIgnore */ public function getRefreshToken(): RefreshTokenEntityInterface { return $this->refreshToken; } } ================================================ FILE: src/RequestTypes/AuthorizationRequest.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\RequestTypes; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; class AuthorizationRequest implements AuthorizationRequestInterface { /** * The grant type identifier */ protected string $grantTypeId; /** * The client identifier */ protected ClientEntityInterface $client; /** * The user identifier */ protected UserEntityInterface $user; /** * An array of scope identifiers * * @var ScopeEntityInterface[] */ protected array $scopes = []; /** * Has the user authorized the authorization request */ protected bool $authorizationApproved = false; /** * The redirect URI used in the request */ protected ?string $redirectUri = null; /** * The state parameter on the authorization request */ protected ?string $state = null; /** * The code challenge (if provided) */ protected string $codeChallenge; /** * The code challenge method (if provided) */ protected string $codeChallengeMethod; public function getGrantTypeId(): string { return $this->grantTypeId; } public function setGrantTypeId(string $grantTypeId): void { $this->grantTypeId = $grantTypeId; } public function getClient(): ClientEntityInterface { return $this->client; } public function setClient(ClientEntityInterface $client): void { $this->client = $client; } public function getUser(): ?UserEntityInterface { return $this->user ?? null; } public function setUser(UserEntityInterface $user): void { $this->user = $user; } /** * @return ScopeEntityInterface[] */ public function getScopes(): array { return $this->scopes; } /** * @param ScopeEntityInterface[] $scopes */ public function setScopes(array $scopes): void { $this->scopes = $scopes; } public function isAuthorizationApproved(): bool { return $this->authorizationApproved; } public function setAuthorizationApproved(bool $authorizationApproved): void { $this->authorizationApproved = $authorizationApproved; } public function getRedirectUri(): ?string { return $this->redirectUri; } public function setRedirectUri(?string $redirectUri): void { $this->redirectUri = $redirectUri; } public function getState(): ?string { return $this->state; } public function setState(string $state): void { $this->state = $state; } public function getCodeChallenge(): ?string { return $this->codeChallenge ?? null; } public function setCodeChallenge(string $codeChallenge): void { $this->codeChallenge = $codeChallenge; } public function getCodeChallengeMethod(): ?string { return $this->codeChallengeMethod ?? null; } public function setCodeChallengeMethod(string $codeChallengeMethod): void { $this->codeChallengeMethod = $codeChallengeMethod; } } ================================================ FILE: src/RequestTypes/AuthorizationRequestInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\RequestTypes; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; interface AuthorizationRequestInterface { public function getUser(): UserEntityInterface|null; public function setState(string $state): void; public function getClient(): ClientEntityInterface; public function setAuthorizationApproved(bool $authorizationApproved): void; /** * @param ScopeEntityInterface[] $scopes */ public function setScopes(array $scopes): void; public function setRedirectUri(?string $redirectUri): void; public function getRedirectUri(): ?string; public function getCodeChallengeMethod(): ?string; public function setGrantTypeId(string $grantTypeId): void; public function setUser(UserEntityInterface $user): void; public function setClient(ClientEntityInterface $client): void; public function setCodeChallenge(string $codeChallenge): void; public function isAuthorizationApproved(): bool; public function getState(): ?string; public function getCodeChallenge(): ?string; public function setCodeChallengeMethod(string $codeChallengeMethod): void; /** * @return ScopeEntityInterface[] */ public function getScopes(): array; public function getGrantTypeId(): string; } ================================================ FILE: src/ResourceServer.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server; use League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface; use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Psr\Http\Message\ServerRequestInterface; class ResourceServer { private CryptKeyInterface $publicKey; public function __construct( private AccessTokenRepositoryInterface $accessTokenRepository, CryptKeyInterface|string $publicKey, private ?AuthorizationValidatorInterface $authorizationValidator = null ) { if ($publicKey instanceof CryptKeyInterface === false) { $publicKey = new CryptKey($publicKey); } $this->publicKey = $publicKey; } protected function getAuthorizationValidator(): AuthorizationValidatorInterface { if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) { $this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository); } if ($this->authorizationValidator instanceof BearerTokenValidator === true) { $this->authorizationValidator->setPublicKey($this->publicKey); } return $this->authorizationValidator; } /** * Determine the access token validity. * * @throws OAuthServerException */ public function validateAuthenticatedRequest(ServerRequestInterface $request): ServerRequestInterface { return $this->getAuthorizationValidator()->validateAuthorization($request); } } ================================================ FILE: src/ResponseTypes/AbstractResponseType.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\ResponseTypes; use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use SensitiveParameter; abstract class AbstractResponseType implements ResponseTypeInterface { use CryptTrait; protected AccessTokenEntityInterface $accessToken; protected RefreshTokenEntityInterface $refreshToken; protected CryptKeyInterface $privateKey; public function setAccessToken( #[SensitiveParameter] AccessTokenEntityInterface $accessToken ): void { $this->accessToken = $accessToken; } public function setRefreshToken( #[SensitiveParameter] RefreshTokenEntityInterface $refreshToken ): void { $this->refreshToken = $refreshToken; } public function setPrivateKey( #[SensitiveParameter] CryptKeyInterface $key ): void { $this->privateKey = $key; } } ================================================ FILE: src/ResponseTypes/BearerTokenResponse.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\ResponseTypes; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use LogicException; use Psr\Http\Message\ResponseInterface; use SensitiveParameter; use function array_merge; use function json_encode; use function time; class BearerTokenResponse extends AbstractResponseType { public function generateHttpResponse(ResponseInterface $response): ResponseInterface { $expireDateTime = $this->accessToken->getExpiryDateTime()->getTimestamp(); $responseParams = [ 'token_type' => 'Bearer', 'expires_in' => $expireDateTime - time(), 'access_token' => $this->accessToken->toString(), ]; if (isset($this->refreshToken)) { $refreshTokenPayload = json_encode([ 'client_id' => $this->accessToken->getClient()->getIdentifier(), 'refresh_token_id' => $this->refreshToken->getIdentifier(), 'access_token_id' => $this->accessToken->getIdentifier(), 'scopes' => $this->accessToken->getScopes(), 'user_id' => $this->accessToken->getUserIdentifier(), 'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(), ]); if ($refreshTokenPayload === false) { throw new LogicException('Error encountered JSON encoding the refresh token payload'); } $responseParams['refresh_token'] = $this->encrypt($refreshTokenPayload); } $responseParams = json_encode(array_merge($this->getExtraParams($this->accessToken), $responseParams)); if ($responseParams === false) { throw new LogicException('Error encountered JSON encoding response parameters'); } $response = $response ->withStatus(200) ->withHeader('pragma', 'no-cache') ->withHeader('cache-control', 'no-store') ->withHeader('content-type', 'application/json; charset=UTF-8'); $response->getBody()->write($responseParams); return $response; } /** * Add custom fields to your Bearer Token response here, then override * AuthorizationServer::getResponseType() to pull in your version of * this class rather than the default. * * @return array */ protected function getExtraParams( #[SensitiveParameter] AccessTokenEntityInterface $accessToken ): array { return []; } } ================================================ FILE: src/ResponseTypes/DeviceCodeResponse.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\ResponseTypes; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; use LogicException; use Psr\Http\Message\ResponseInterface; use function json_encode; use function time; class DeviceCodeResponse extends AbstractResponseType { protected DeviceCodeEntityInterface $deviceCodeEntity; private bool $includeVerificationUriComplete = false; private bool $includeInterval = false; /** * {@inheritdoc} */ public function generateHttpResponse(ResponseInterface $response): ResponseInterface { $expireDateTime = $this->deviceCodeEntity->getExpiryDateTime()->getTimestamp(); $responseParams = [ 'device_code' => $this->deviceCodeEntity->getIdentifier(), 'user_code' => $this->deviceCodeEntity->getUserCode(), 'verification_uri' => $this->deviceCodeEntity->getVerificationUri(), 'expires_in' => $expireDateTime - time(), ]; if ($this->includeVerificationUriComplete === true) { $responseParams['verification_uri_complete'] = $this->deviceCodeEntity->getVerificationUriComplete(); } if ($this->includeInterval === true) { $responseParams['interval'] = $this->deviceCodeEntity->getInterval(); } $responseParams = json_encode($responseParams); if ($responseParams === false) { throw new LogicException('Error encountered JSON encoding response parameters'); } $response = $response ->withStatus(200) ->withHeader('pragma', 'no-cache') ->withHeader('cache-control', 'no-store') ->withHeader('content-type', 'application/json; charset=UTF-8'); $response->getBody()->write($responseParams); return $response; } /** * {@inheritdoc} */ public function setDeviceCodeEntity(DeviceCodeEntityInterface $deviceCodeEntity): void { $this->deviceCodeEntity = $deviceCodeEntity; } public function includeVerificationUriComplete(): void { $this->includeVerificationUriComplete = true; } public function includeInterval(): void { $this->includeInterval = true; } /** * Add custom fields to your Bearer Token response here, then override * AuthorizationServer::getResponseType() to pull in your version of * this class rather than the default. * * @return array */ protected function getExtraParams(DeviceCodeEntityInterface $deviceCode): array { return []; } } ================================================ FILE: src/ResponseTypes/RedirectResponse.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\ResponseTypes; use Psr\Http\Message\ResponseInterface; class RedirectResponse extends AbstractResponseType { private string $redirectUri; public function setRedirectUri(string $redirectUri): void { $this->redirectUri = $redirectUri; } public function generateHttpResponse(ResponseInterface $response): ResponseInterface { return $response->withStatus(302)->withHeader('Location', $this->redirectUri); } } ================================================ FILE: src/ResponseTypes/ResponseTypeInterface.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ declare(strict_types=1); namespace League\OAuth2\Server\ResponseTypes; use Defuse\Crypto\Key; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use Psr\Http\Message\ResponseInterface; interface ResponseTypeInterface { public function setAccessToken(AccessTokenEntityInterface $accessToken): void; public function setRefreshToken(RefreshTokenEntityInterface $refreshToken): void; public function generateHttpResponse(ResponseInterface $response): ResponseInterface; public function setEncryptionKey(Key|string|null $key = null): void; } ================================================ FILE: tests/AuthorizationServerTest.php ================================================ getMockBuilder(ClientRepositoryInterface::class)->getMock(), $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/Stubs/private.key', base64_encode(random_bytes(36)), new StubResponseType() ); $server->enableGrantType(new GrantType(), new DateInterval('PT1M')); $authRequest = $server->validateAuthorizationRequest($this->createMock(ServerRequestInterface::class)); self::assertSame(GrantType::class, $authRequest->getGrantTypeId()); } public function testRespondToRequestInvalidGrantType(): void { $server = new AuthorizationServer( $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(), $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/Stubs/private.key', base64_encode(random_bytes(36)), new StubResponseType() ); $server->enableGrantType(new ClientCredentialsGrant(), new DateInterval('PT1M')); try { $server->respondToAccessTokenRequest(ServerRequestFactory::fromGlobals(), new Response()); } catch (OAuthServerException $e) { self::assertEquals('unsupported_grant_type', $e->getErrorType()); self::assertEquals(400, $e->getHttpStatusCode()); } } public function testRespondToRequest(): void { $client = new ClientEntity(); $client->setConfidential(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepository->method('getClientEntity')->willReturn($client); $clientRepository->method('validateClient')->willReturn(true); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $server = new AuthorizationServer( $clientRepository, $accessTokenRepositoryMock, $scopeRepositoryMock, 'file://' . __DIR__ . '/Stubs/private.key', base64_encode(random_bytes(36)), new StubResponseType() ); $server->setDefaultScope(self::DEFAULT_SCOPE); $server->enableGrantType(new ClientCredentialsGrant(), new DateInterval('PT1M')); $_POST['grant_type'] = 'client_credentials'; $_POST['client_id'] = 'foo'; $_POST['client_secret'] = 'bar'; $response = $server->respondToAccessTokenRequest(ServerRequestFactory::fromGlobals(), new Response()); self::assertEquals(200, $response->getStatusCode()); } public function testGetResponseType(): void { $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $server = new AuthorizationServer( $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/Stubs/private.key', 'file://' . __DIR__ . '/Stubs/public.key' ); $abstractGrantReflection = new ReflectionClass($server); $method = $abstractGrantReflection->getMethod('getResponseType'); self::assertInstanceOf(BearerTokenResponse::class, $method->invoke($server)); } public function testGetResponseTypeExtended(): void { $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $privateKey = 'file://' . __DIR__ . '/Stubs/private.key'; $encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key'; $server = new AuthorizationServer( $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/Stubs/private.key', 'file://' . __DIR__ . '/Stubs/public.key' ); $abstractGrantReflection = new ReflectionClass($server); $method = $abstractGrantReflection->getMethod('getResponseType'); $responseType = $method->invoke($server); $responseTypeReflection = new ReflectionClass($responseType); $privateKeyProperty = $responseTypeReflection->getProperty('privateKey'); $encryptionKeyProperty = $responseTypeReflection->getProperty('encryptionKey'); // generated instances should have keys setup self::assertSame($privateKey, $privateKeyProperty->getValue($responseType)->getKeyPath()); self::assertSame($encryptionKey, $encryptionKeyProperty->getValue($responseType)); } public function testMultipleRequestsGetDifferentResponseTypeInstances(): void { $privateKey = 'file://' . __DIR__ . '/Stubs/private.key'; $encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key'; $responseTypePrototype = new class () extends BearerTokenResponse { protected CryptKeyInterface $privateKey; protected Key|string|null $encryptionKey = null; public function getPrivateKey(): CryptKeyInterface { return $this->privateKey; } public function getEncryptionKey(): Key|string|null { return $this->encryptionKey; } }; $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $server = new AuthorizationServer( $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), $privateKey, $encryptionKey, $responseTypePrototype ); $abstractGrantReflection = new ReflectionClass($server); $method = $abstractGrantReflection->getMethod('getResponseType'); $responseTypeA = $method->invoke($server); $responseTypeB = $method->invoke($server); // generated instances should have keys setup self::assertSame($privateKey, $responseTypeA->getPrivateKey()->getKeyPath()); self::assertSame($encryptionKey, $responseTypeA->getEncryptionKey()); // all instances should be different but based on the same prototype self::assertSame(get_class($responseTypePrototype), get_class($responseTypeA)); self::assertSame(get_class($responseTypePrototype), get_class($responseTypeB)); self::assertNotSame($responseTypePrototype, $responseTypeA); self::assertNotSame($responseTypePrototype, $responseTypeB); self::assertNotSame($responseTypeA, $responseTypeB); } public function testCompleteAuthorizationRequest(): void { $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $server = new AuthorizationServer( $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/Stubs/private.key', 'file://' . __DIR__ . '/Stubs/public.key' ); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $server->enableGrantType($grant); $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $client->setIdentifier('clientId'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $response = $server->completeAuthorizationRequest($authRequest, new Response()); $locationHeader = $response->getHeader('Location')[0]; self::assertStringStartsWith('http://foo/bar', $locationHeader); self::assertStringContainsString('code=', $locationHeader); } public function testValidateAuthorizationRequest(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $server = new AuthorizationServer( $clientRepositoryMock, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $scopeRepositoryMock, 'file://' . __DIR__ . '/Stubs/private.key', 'file://' . __DIR__ . '/Stubs/public.key' ); $server->setDefaultScope(self::DEFAULT_SCOPE); $server->enableGrantType($grant); $request = new ServerRequest( [], [], null, null, 'php://input', $headers = [], $cookies = [], $queryParams = [ 'response_type' => 'code', 'client_id' => 'foo', ] ); self::assertInstanceOf(AuthorizationRequest::class, $server->validateAuthorizationRequest($request)); } public function testValidateAuthorizationRequestUnregistered(): void { $server = new AuthorizationServer( $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(), $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/Stubs/private.key', 'file://' . __DIR__ . '/Stubs/public.key' ); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(2); $server->validateAuthorizationRequest($request); } } ================================================ FILE: tests/AuthorizationValidators/BearerTokenValidatorTest.php ================================================ getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() ->permittedFor('client-id') ->identifiedBy('token-id') ->issuedAt(new DateTimeImmutable()) ->canOnlyBeUsedAfter(new DateTimeImmutable()) ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) ->relatedTo('user-id') ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $validJwt->toString())); $validRequest = $bearerTokenValidator->validateAuthorization($request); self::assertArrayHasKey('authorization', $validRequest->getHeaders()); } public function testBearerTokenValidatorRejectsExpiredToken(): void { $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); $expiredJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() ->permittedFor('client-id') ->identifiedBy('token-id') ->issuedAt(new DateTimeImmutable()) ->canOnlyBeUsedAfter(new DateTimeImmutable()) ->expiresAt((new DateTimeImmutable())->sub(new DateInterval('PT1H'))) ->relatedTo('user-id') ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $expiredJwt->toString())); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(9); $bearerTokenValidator->validateAuthorization($request); } public function testBearerTokenValidatorAcceptsExpiredTokenWithinLeeway(): void { $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); // We fake generating this token 10 seconds into the future, an extreme example of possible time drift between servers $future = (new DateTimeImmutable())->add(new DateInterval('PT10S')); $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock, new DateInterval('PT10S')); $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); $jwtTokenFromFutureWithinLeeway = $jwtConfiguration->getValue($bearerTokenValidator)->builder() ->permittedFor('client-id') ->identifiedBy('token-id') ->issuedAt($future) ->canOnlyBeUsedAfter($future) ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) ->relatedTo('user-id') ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $jwtTokenFromFutureWithinLeeway->toString())); $validRequest = $bearerTokenValidator->validateAuthorization($request); self::assertArrayHasKey('authorization', $validRequest->getHeaders()); } public function testBearerTokenValidatorRejectsExpiredTokenBeyondLeeway(): void { $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); // We fake generating this token 20 seconds into the future, an extreme example of possible time drift between servers $future = (new DateTimeImmutable())->add(new DateInterval('PT20S')); $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock, new DateInterval('PT10S')); $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); $jwtTokenFromFutureBeyondLeeway = $jwtConfiguration->getValue($bearerTokenValidator)->builder() ->permittedFor('client-id') ->identifiedBy('token-id') ->issuedAt($future) ->canOnlyBeUsedAfter($future) ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) ->relatedTo('user-id') ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $jwtTokenFromFutureBeyondLeeway->toString())); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(9); $bearerTokenValidator->validateAuthorization($request); } public function testBearerTokenValidatorCaseInsensitiveWithBearerHeader(): void { $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock); $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder() ->permittedFor('client-id') ->identifiedBy('token-id') ->issuedAt(new DateTimeImmutable()) ->canOnlyBeUsedAfter(new DateTimeImmutable()) ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) ->relatedTo('user-id') ->withClaim('scopes', 'scope1 scope2 scope3 scope4') ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('bEaReR %s', $validJwt->toString())); $validRequest = $bearerTokenValidator->validateAuthorization($request); self::assertArrayHasKey('authorization', $validRequest->getHeaders()); } } ================================================ FILE: tests/CodeChallengeVerifiers/PlainVerifierTest.php ================================================ getMethod()); } public function testVerifyCodeChallenge(): void { $verifier = new PlainVerifier(); self::assertTrue($verifier->verifyCodeChallenge('foo', 'foo')); self::assertFalse($verifier->verifyCodeChallenge('foo', 'bar')); } } ================================================ FILE: tests/CodeChallengeVerifiers/S256VerifierTest.php ================================================ getMethod()); } public function testVerifyCodeChallengeSucceeds(): void { $codeChallenge = $this->createCodeChallenge('foo'); $verifier = new S256Verifier(); self::assertTrue($verifier->verifyCodeChallenge('foo', $codeChallenge)); } public function testVerifyCodeChallengeFails(): void { $codeChallenge = $this->createCodeChallenge('bar'); $verifier = new S256Verifier(); self::assertFalse($verifier->verifyCodeChallenge('foo', $codeChallenge)); } private function createCodeChallenge(string $codeVerifier): string { return strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'); } } ================================================ FILE: tests/EventEmitting/EmitterAwarePolyfillTest.php ================================================ getEmitter(); self::assertSame( $emitter, $emitterAwarePolyfill->getEmitter(), 'The emitter should be the same instance' ); self::assertSame( $emitter, $emitterAwarePolyfill->getEventDispatcher(), 'The event dispatcher should be the same instance' ); self::assertSame( $emitter, $emitterAwarePolyfill->getListenerRegistry(), 'The listener registry should be the same instance' ); // manually set $emitter = new EventEmitter(); $emitterAwarePolyfill->setEmitter($emitter); self::assertSame( $emitter, $emitterAwarePolyfill->getEmitter(), 'The emitter should be the same instance' ); self::assertSame( $emitter, $emitterAwarePolyfill->getEventDispatcher(), 'The event dispatcher should be the same instance' ); self::assertSame( $emitter, $emitterAwarePolyfill->getListenerRegistry(), 'The listener registry should be the same instance' ); } } ================================================ FILE: tests/Exception/OAuthServerExceptionTest.php ================================================ withParsedBody([ 'client_id' => 'foo', ]) ->withAddedHeader('Authorization', 'Basic fakeauthdetails'); try { $this->issueInvalidClientException($serverRequest); } catch (OAuthServerException $e) { $response = $e->generateHttpResponse(new Response()); self::assertTrue($response->hasHeader('WWW-Authenticate')); } } public function testInvalidClientExceptionSetsBearerAuthenticateHeader(): void { $serverRequest = (new ServerRequest()) ->withParsedBody([ 'client_id' => 'foo', ]) ->withAddedHeader('Authorization', 'Bearer fakeauthdetails'); try { $this->issueInvalidClientException($serverRequest); } catch (OAuthServerException $e) { $response = $e->generateHttpResponse(new Response()); self::assertEquals(['Bearer realm="OAuth"'], $response->getHeader('WWW-Authenticate')); } } public function testInvalidClientExceptionOmitsAuthenticateHeader(): void { $serverRequest = (new ServerRequest()) ->withParsedBody([ 'client_id' => 'foo', ]); try { $this->issueInvalidClientException($serverRequest); } catch (OAuthServerException $e) { $response = $e->generateHttpResponse(new Response()); self::assertFalse($response->hasHeader('WWW-Authenticate')); } } public function testInvalidClientExceptionOmitsAuthenticateHeaderGivenEmptyAuthorizationHeader(): void { $serverRequest = (new ServerRequest()) ->withParsedBody([ 'client_id' => 'foo', ]) ->withAddedHeader('Authorization', ''); try { $this->issueInvalidClientException($serverRequest); } catch (OAuthServerException $e) { $response = $e->generateHttpResponse(new Response()); self::assertFalse($response->hasHeader('WWW-Authenticate')); } } /** * Issue an invalid client exception * * @throws OAuthServerException */ private function issueInvalidClientException(ServerRequestInterface $serverRequest): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('validateClient')->willReturn(false); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $validateClientMethod->invoke($grantMock, $serverRequest); } public function testHasRedirect(): void { $exceptionWithRedirect = OAuthServerException::accessDenied('some hint', 'https://example.com/error'); self::assertTrue($exceptionWithRedirect->hasRedirect()); } public function testDoesNotHaveRedirect(): void { $exceptionWithoutRedirect = OAuthServerException::accessDenied('Some hint'); self::assertFalse($exceptionWithoutRedirect->hasRedirect()); } public function testHasPrevious(): void { $previous = new Exception('This is the previous'); $exceptionWithPrevious = OAuthServerException::accessDenied(null, null, $previous); $previousMessage = $exceptionWithPrevious->getPrevious()?->getMessage(); self::assertSame('This is the previous', $previousMessage); } public function testDoesNotHavePrevious(): void { $exceptionWithoutPrevious = OAuthServerException::accessDenied(); self::assertNull($exceptionWithoutPrevious->getPrevious()); } public function testCanGetRedirectionUri(): void { $exceptionWithRedirect = OAuthServerException::accessDenied('some hint', 'https://example.com/error'); self::assertSame('https://example.com/error', $exceptionWithRedirect->getRedirectUri()); } public function testInvalidCredentialsIsInvalidGrant(): void { $exception = OAuthServerException::invalidCredentials(); self::assertSame('invalid_grant', $exception->getErrorType()); } } ================================================ FILE: tests/Grant/AbstractGrantTest.php ================================================ getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ' . base64_encode('Open:Sesame')); $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); self::assertSame(['Open', 'Sesame'], $basicAuthMethod->invoke($grantMock, $serverRequest)); } public function testHttpBasicNoPassword(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ' . base64_encode('Open:')); $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); self::assertSame(['Open', ''], $basicAuthMethod->invoke($grantMock, $serverRequest)); } public function testHttpBasicNotBasic(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withHeader('Authorization', 'Foo ' . base64_encode('Open:Sesame')); $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); self::assertSame([null, null], $basicAuthMethod->invoke($grantMock, $serverRequest)); } public function testHttpBasicCaseInsensitive(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withHeader('Authorization', 'bAsIc ' . base64_encode('Open:Sesame')); $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); self::assertSame(['Open', 'Sesame'], $basicAuthMethod->invoke($grantMock, $serverRequest)); } public function testHttpBasicNotBase64(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ||'); $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); self::assertSame([null, null], $basicAuthMethod->invoke($grantMock, $serverRequest)); } public function testHttpBasicNoColon(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ' . base64_encode('OpenSesame')); $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); self::assertSame([null, null], $basicAuthMethod->invoke($grantMock, $serverRequest)); } public function testGetClientCredentialsClientSecretNotAString(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'client_id' => 'client_id', 'client_secret' => ['not', 'a', 'string'], ] ); $getClientCredentialsMethod = $abstractGrantReflection->getMethod('getClientCredentials'); $this->expectException(OAuthServerException::class); $getClientCredentialsMethod->invoke($grantMock, $serverRequest, true, true); } public function testValidateClientPublic(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', ]); $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $result = $validateClientMethod->invoke($grantMock, $serverRequest); self::assertEquals($client, $result); } public function testValidateClientConfidential(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => 'http://foo/bar', ]); $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $result = $validateClientMethod->invoke($grantMock, $serverRequest, true, true); self::assertEquals($client, $result); } public function testValidateClientMissingClientId(): void { $client = new ClientEntity(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = new ServerRequest(); $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $this->expectException(OAuthServerException::class); $validateClientMethod->invoke($grantMock, $serverRequest, true, true); } public function testValidateClientMissingClientSecret(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('validateClient')->willReturn(false); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', ]); $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $this->expectException(OAuthServerException::class); $validateClientMethod->invoke($grantMock, $serverRequest, true, true); } public function testValidateClientInvalidClientSecret(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('validateClient')->willReturn(false); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'foo', ]); $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $this->expectException(OAuthServerException::class); $validateClientMethod->invoke($grantMock, $serverRequest, true, true); } public function testValidateClientBadClient(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('validateClient')->willReturn(false); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', ]); $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $this->expectException(OAuthServerException::class); $validateClientMethod->invoke($grantMock, $serverRequest, true); } public function testCanRespondToRequest(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->method('getIdentifier')->willReturn('foobar'); $grantMock->setDefaultScope('defaultScope'); $serverRequest = (new ServerRequest())->withParsedBody([ 'grant_type' => 'foobar', ]); self::assertTrue($grantMock->canRespondToAccessTokenRequest($serverRequest)); } public function testIssueRefreshToken(): void { $refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepoMock ->expects(self::once()) ->method('getNewRefreshToken') ->willReturn(new RefreshTokenEntity()); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setRefreshTokenTTL(new DateInterval('PT1M')); $grantMock->setRefreshTokenRepository($refreshTokenRepoMock); $abstractGrantReflection = new ReflectionClass($grantMock); $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken'); $accessToken = new AccessTokenEntity(); $accessToken->setClient(new ClientEntity()); /** @var RefreshTokenEntityInterface $refreshToken */ $refreshToken = $issueRefreshTokenMethod->invoke($grantMock, $accessToken); self::assertEquals($accessToken, $refreshToken->getAccessToken()); } public function testIssueNullRefreshToken(): void { $refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepoMock ->expects(self::once()) ->method('getNewRefreshToken') ->willReturn(null); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setRefreshTokenTTL(new DateInterval('PT1M')); $grantMock->setRefreshTokenRepository($refreshTokenRepoMock); $abstractGrantReflection = new ReflectionClass($grantMock); $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken'); $accessToken = new AccessTokenEntity(); $accessToken->setClient(new ClientEntity()); self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken)); } public function testIssueNullRefreshTokenUnauthorizedClient(): void { $client = $this->getMockBuilder(ClientEntity::class)->getMock(); $client ->expects(self::once()) ->method('supportsGrantType') ->with('refresh_token') ->willReturn(false); $refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepoMock->expects(self::never())->method('getNewRefreshToken'); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setRefreshTokenTTL(new DateInterval('PT1M')); $grantMock->setRefreshTokenRepository($refreshTokenRepoMock); $abstractGrantReflection = new ReflectionClass($grantMock); $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken'); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken)); } public function testIssueAccessToken(): void { $accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepoMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grantMock->setAccessTokenRepository($accessTokenRepoMock); $abstractGrantReflection = new ReflectionClass($grantMock); $issueAccessTokenMethod = $abstractGrantReflection->getMethod('issueAccessToken'); /** @var AccessTokenEntityInterface $accessToken */ $accessToken = $issueAccessTokenMethod->invoke( $grantMock, new DateInterval('PT1H'), new ClientEntity(), 123, [new ScopeEntity()] ); self::assertNotEmpty($accessToken->getIdentifier()); } public function testIssueAuthCode(): void { $authCodeRepoMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepoMock->expects(self::once())->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setAuthCodeRepository($authCodeRepoMock); $abstractGrantReflection = new ReflectionClass($grantMock); $issueAuthCodeMethod = $abstractGrantReflection->getMethod('issueAuthCode'); $scope = new ScopeEntity(); $scope->setIdentifier('scopeId'); self::assertInstanceOf( AuthCodeEntityInterface::class, $issueAuthCodeMethod->invoke( $grantMock, new DateInterval('PT1H'), new ClientEntity(), 123, 'http://foo/bar', [$scope] ) ); } public function testGetCookieParameter(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $method = $abstractGrantReflection->getMethod('getCookieParameter'); $serverRequest = (new ServerRequest())->withCookieParams([ 'foo' => 'bar', ]); self::assertEquals('bar', $method->invoke($grantMock, 'foo', $serverRequest)); self::assertEquals('foo', $method->invoke($grantMock, 'bar', $serverRequest, 'foo')); } public function testGetQueryStringParameter(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $method = $abstractGrantReflection->getMethod('getQueryStringParameter'); $serverRequest = (new ServerRequest())->withQueryParams([ 'foo' => 'bar', ]); self::assertEquals('bar', $method->invoke($grantMock, 'foo', $serverRequest)); self::assertEquals('foo', $method->invoke($grantMock, 'bar', $serverRequest, 'foo')); } public function testValidateScopes(): void { $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->expects(self::exactly(3))->method('getScopeEntityByIdentifier')->willReturn($scope); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setScopeRepository($scopeRepositoryMock); self::assertEquals([$scope, $scope, $scope], $grantMock->validateScopes('basic test 0 ')); } public function testValidateScopesBadScope(): void { $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(null); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setScopeRepository($scopeRepositoryMock); $this->expectException(OAuthServerException::class); $grantMock->validateScopes('basic '); } public function testGenerateUniqueIdentifier(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $abstractGrantReflection = new ReflectionClass($grantMock); $method = $abstractGrantReflection->getMethod('generateUniqueIdentifier'); self::assertIsString($method->invoke($grantMock)); } public function testCanRespondToAuthorizationRequest(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); self::assertFalse($grantMock->canRespondToAuthorizationRequest(new ServerRequest())); } public function testValidateAuthorizationRequest(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $this->expectException(LogicException::class); $grantMock->validateAuthorizationRequest(new ServerRequest()); } public function testCompleteAuthorizationRequest(): void { $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $this->expectException(LogicException::class); $grantMock->completeAuthorizationRequest(new AuthorizationRequest()); } public function testUnauthorizedClient(): void { $client = $this->getMockBuilder(ClientEntity::class)->getMock(); $client->method('supportsGrantType')->willReturn(false); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock ->expects(self::once()) ->method('getClientEntity') ->with('foo') ->willReturn($client); $grantMock = $this->getMockBuilder(AbstractGrant::class) ->onlyMethods(['getIdentifier', 'respondToAccessTokenRequest']) ->getMock(); $grantMock->setClientRepository($clientRepositoryMock); $abstractGrantReflection = new ReflectionClass($grantMock); $getClientEntityOrFailMethod = $abstractGrantReflection->getMethod('getClientEntityOrFail'); $this->expectException(OAuthServerException::class); $getClientEntityOrFailMethod->invoke($grantMock, 'foo', new ServerRequest()); } } ================================================ FILE: tests/Grant/AuthCodeGrantTest.php ================================================ cryptStub = new CryptTraitStub(); } public function testGetIdentifier(): void { $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); self::assertEquals('authorization_code', $grant->getIdentifier()); } public function testCanRespondToAuthorizationRequest(): void { $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $request = new ServerRequest( [], [], null, null, 'php://input', $headers = [], $cookies = [], $queryParams = [ 'response_type' => 'code', 'client_id' => 'foo', ] ); self::assertTrue($grant->canRespondToAuthorizationRequest($request)); } public function testValidateAuthorizationRequest(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = new ServerRequest( [], [], null, null, 'php://input', [], [], [ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, ] ); self::assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request)); } public function testValidateAuthorizationRequestRedirectUriArray(): void { $client = new ClientEntity(); $client->setRedirectUri([self::REDIRECT_URI]); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = new ServerRequest( [], [], null, null, 'php://input', [], [], [ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, ] ); self::assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request)); } public function testValidateAuthorizationRequestWithoutRedirectUri(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = new ServerRequest( [], [], null, null, 'php://input', [], [], [ 'response_type' => 'code', 'client_id' => 'foo', ] ); $authorizationRequest = $grant->validateAuthorizationRequest($request); self::assertInstanceOf(AuthorizationRequest::class, $authorizationRequest); self::assertEmpty($authorizationRequest->getRedirectUri()); } public function testValidateAuthorizationRequestCodeChallenge(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = new ServerRequest( [], [], null, null, 'php://input', [], [], [ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_CHALLENGE, ] ); self::assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request)); } public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooShort(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => str_repeat('A', 42), ]); $this->expectException(OAuthServerException::class); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooLong(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => str_repeat('A', 129), ]); $this->expectException(OAuthServerException::class); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestCodeChallengeInvalidCharacters(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => str_repeat('A', 42) . '!', ]); $this->expectException(OAuthServerException::class); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestMissingClientId(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestInvalidClientId(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn(null); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(4); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestBadRedirectUriString(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => 'http://bar', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(4); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestBadRedirectUriArray(): void { $client = new ClientEntity(); $client->setRedirectUri([self::REDIRECT_URI]); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => 'http://bar', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(4); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestInvalidCodeChallengeMethod(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'foo', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestInvalidScopes(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(null); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'scope' => 'foo', 'state' => 'foo', ]); try { $grant->validateAuthorizationRequest($request); } catch (OAuthServerException $e) { self::assertSame(5, $e->getCode()); self::assertSame('invalid_scope', $e->getErrorType()); self::assertSame('https://foo/bar?state=foo', $e->getRedirectUri()); return; } self::fail('The expected exception was not thrown'); } public function testCompleteAuthorizationRequest(): void { $client = new ClientEntity(); $client->setIdentifier('clientId'); $client->setRedirectUri('http://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setEncryptionKey($this->cryptStub->getKey()); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } public function testCompleteAuthorizationRequestWithMultipleRedirectUrisOnClient(): void { $client = new ClientEntity(); $client->setIdentifier('clientId'); $client->setRedirectUri(['uriOne', 'uriTwo']); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setEncryptionKey($this->cryptStub->getKey()); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } public function testCompleteAuthorizationRequestDenied(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(false); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $authRequest->setState('foo'); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setEncryptionKey($this->cryptStub->getKey()); try { $grant->completeAuthorizationRequest($authRequest); } catch (OAuthServerException $e) { self::assertSame(9, $e->getCode()); self::assertSame('access_denied', $e->getErrorType()); self::assertSame('http://foo/bar?state=foo', $e->getRedirectUri()); return; } self::fail('The expected exception was not thrown'); } public function testRespondToAccessTokenRequest(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $accessTokenEventEmitted = false; $refreshTokenEventEmitted = false; $grant->getListenerRegistry()->subscribeTo( RequestEvent::ACCESS_TOKEN_ISSUED, function ($event) use (&$accessTokenEventEmitted): void { self::assertInstanceOf(RequestAccessTokenEvent::class, $event); $accessTokenEventEmitted = true; } ); $grant->getListenerRegistry()->subscribeTo( RequestEvent::REFRESH_TOKEN_ISSUED, function ($event) use (&$refreshTokenEventEmitted): void { self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); $refreshTokenEventEmitted = true; } ); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); if (!$accessTokenEventEmitted) { self::fail('Access token issued event is not emitted.'); } if (!$refreshTokenEventEmitted) { self::fail('Refresh token issued event is not emitted.'); } } public function testRespondToAccessTokenRequestWithDefaultRedirectUri(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => null, ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testRespondToAccessTokenRequestUsingHttpBasicAuth(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $authCodeGrant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $refreshTokenRepositoryMock, new DateInterval('PT10M') ); $authCodeGrant->setClientRepository($clientRepositoryMock); $authCodeGrant->setScopeRepository($scopeRepositoryMock); $authCodeGrant->setAccessTokenRepository($accessTokenRepositoryMock); $authCodeGrant->setEncryptionKey($this->cryptStub->getKey()); $authCodeGrant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [ 'Authorization' => 'Basic Zm9vOmJhcg==', ], [], [], [ 'grant_type' => 'authorization_code', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'client_id' => 'foo', 'expire_time' => time() + 3600, 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $authCodeGrant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testRespondToAccessTokenRequestForPublicClient(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testRespondToAccessTokenRequestNullRefreshToken(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $refreshTokenRepositoryMock, new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertNull($response->getRefreshToken()); } public function testRespondToAccessTokenRequestCodeChallengePlain(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_VERIFIER, 'code_challenge_method' => 'plain', ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testRespondToAccessTokenRequestCodeChallengeS256(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_CHALLENGE, 'code_challenge_method' => 'S256', ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testPKCEDowngradeBlocked(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, 'code' => $this->cryptStub->doEncrypt( json_encode( [ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR ) ), ] ); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } public function testRespondToAccessTokenRequestMissingRedirectUri(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setConfidential(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'client_id' => 'foo', 'grant_type' => 'authorization_code', 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'redirect_uri' => 'http://foo/bar', ], JSON_THROW_ON_ERROR) ), ] ); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } public function testRespondToAccessTokenRequestRedirectUriMismatch(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setConfidential(); $client->setRedirectUri('http://bar/foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'client_id' => 'foo', 'grant_type' => 'authorization_code', 'redirect_uri' => 'http://bar/foo', 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'redirect_uri' => 'http://foo/bar', ], JSON_THROW_ON_ERROR) ), ] ); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } public function testRejectAccessTokenRequestIfRedirectUriSpecifiedButNotInOriginalAuthCodeRequest(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setConfidential(); $client->setRedirectUri('http://bar/foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'client_id' => 'foo', 'grant_type' => 'authorization_code', 'redirect_uri' => 'http://bar/foo', 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'redirect_uri' => null, ], JSON_THROW_ON_ERROR) ), ] ); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } public function testRespondToAccessTokenRequestMissingCode(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, ] ); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } public function testRespondToAccessTokenRequestWithRefreshTokenInsteadOfAuthCode(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => 123, 'expire_time' => time() + 3600, ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals('Authorization code malformed', $e->getHint()); } } public function testRespondToAccessTokenRequestWithAuthCodeNotAString(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => ['not', 'a', 'string'], ] ); $this->expectException(OAuthServerException::class); $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } public function testRespondToAccessTokenRequestExpiredCode(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() - 3600, 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], 'redirect_uri' => 'http://foo/bar', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Authorization code has expired'); } } public function testRespondToAccessTokenRequestRevokedCode(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepositoryMock->method('isAuthCodeRevoked')->willReturn(true); $grant = new AuthCodeGrant( $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], 'redirect_uri' => 'http://foo/bar', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Authorization code has been revoked'); self::assertEquals($e->getErrorType(), 'invalid_grant'); } } public function testRespondToAccessTokenRequestClientMismatch(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'bar', 'user_id' => 123, 'scopes' => ['foo'], 'redirect_uri' => 'http://foo/bar', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Authorization code was not issued to this client'); } } public function testRespondToAccessTokenRequestBadCode(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code' => 'badCode', ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getErrorType(), 'invalid_grant'); self::assertEquals($e->getHint(), 'Cannot validate the provided authorization code'); } } public function testRespondToAccessTokenRequestNoEncryptionKey(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); // We deliberately don't set an encryption key here $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code' => 'badCode', ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getErrorType(), 'invalid_request'); self::assertEquals($e->getHint(), 'Issue decrypting the authorization code'); } } public function testRespondToAccessTokenRequestBadCodeVerifierPlain(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'plain', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Failed to verify `code_verifier`.'); } } public function testRespondToAccessTokenRequestBadCodeVerifierS256(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'nope', 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'S256', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Code Verifier must follow the specifications of RFC-7636.'); } } public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInvalidChars(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'dqX7C-RbqjHYtytmhGTigKdZCXfxq-+xbsk9_GxUcaE', // Malformed code. Contains `+`. 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_CHALLENGE, 'code_challenge_method' => 'S256', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Code Verifier must follow the specifications of RFC-7636.'); } } public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInvalidLength(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'dqX7C-RbqjHY', // Malformed code. Invalid length. 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'R7T1y1HPNFvs1WDCrx4lfoBS6KD2c71pr8OHvULjvv8', 'code_challenge_method' => 'S256', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Code Verifier must follow the specifications of RFC-7636.'); } } public function testRespondToAccessTokenRequestMissingCodeVerifier(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'plain', ], JSON_THROW_ON_ERROR) ), ] ); try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getHint(), 'Check the `code_verifier` parameter'); } } public function testAuthCodeRepositoryUniqueConstraintCheck(): void { $client = new ClientEntity(); $client->setIdentifier('clientId'); $client->setRedirectUri(self::REDIRECT_URI); $client->setIdentifier('clientIdentifier'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $authCodeRepository ->expects(self::exactly(2)) ->method('persistNewAuthCode') ->willReturnCallback(function (): void { static $counter = 0; if (1 === ++$counter) { throw UniqueTokenIdentifierConstraintViolationException::create(); } }); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } public function testAuthCodeRepositoryFailToPersist(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $authCodeRepository->method('persistNewAuthCode')->willThrowException(OAuthServerException::serverError('something bad happened')); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setEncryptionKey($this->cryptStub->getKey()); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(7); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } public function testAuthCodeRepositoryFailToPersistUniqueNoInfiniteLoop(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $authCodeRepository->method('persistNewAuthCode')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $this->expectException(UniqueTokenIdentifierConstraintViolationException::class); $this->expectExceptionCode(100); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } public function testRefreshTokenRepositoryUniqueConstraintCheck(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock ->expects(self::exactly(2)) ->method('persistNewRefreshToken') ->willReturnCallback(function (): void { static $count = 0; if (1 === ++$count) { throw UniqueTokenIdentifierConstraintViolationException::create(); } }); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR) ), ] ); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testRefreshTokenRepositoryFailToPersist(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willThrowException(OAuthServerException::serverError('something bad happened')); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR) ), ] ); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(7); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], [], null, 'POST', 'php://input', [], [], [], [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( json_encode([ 'auth_code_id' => uniqid(), 'expire_time' => time() + 3600, 'client_id' => 'foo', 'user_id' => '123', 'scopes' => ['foo'], 'redirect_uri' => self::REDIRECT_URI, ], JSON_THROW_ON_ERROR) ), ] ); $this->expectException(UniqueTokenIdentifierConstraintViolationException::class); $this->expectExceptionCode(100); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } public function testCompleteAuthorizationRequestNoUser(): void { $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $this->expectException(LogicException::class); $grant->completeAuthorizationRequest(new AuthorizationRequest()); } public function testPublicClientAuthCodeRequestRejectedWhenCodeChallengeRequiredButNotGiven(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'state' => 'foo', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->validateAuthorizationRequest($request); } public function testUseValidRedirectUriIfScopeCheckFails(): void { $client = new ClientEntity(); $client->setRedirectUri([self::REDIRECT_URI, 'http://bar/foo']); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(null); $grant = new AuthCodeGrant( $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = new ServerRequest( [], [], null, null, 'php://input', [], [], [ 'response_type' => 'code', 'client_id' => 'foo', 'redirect_uri' => 'http://bar/foo', ] ); // At this point I need to validate the auth request try { $grant->validateAuthorizationRequest($request); } catch (OAuthServerException $e) { $response = $e->generateHttpResponse(new Response()); self::assertStringStartsWith('http://bar/foo', $response->getHeader('Location')[0]); } } } ================================================ FILE: tests/Grant/ClientCredentialsGrantTest.php ================================================ getIdentifier()); } public function testRespondToRequest(): void { $client = new ClientEntity(); $client->setConfidential(); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ClientCredentialsGrant(); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $accessTokenEventEmitted = false; $grant->getListenerRegistry()->subscribeTo( RequestEvent::ACCESS_TOKEN_ISSUED, function ($event) use (&$accessTokenEventEmitted): void { self::assertInstanceOf(RequestAccessTokenEvent::class, $event); $accessTokenEventEmitted = true; } ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', ]); $responseType = new StubResponseType(); /** @var StubResponseType $response */ $response = $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertNotEmpty($response->getAccessToken()->getIdentifier()); if (!$accessTokenEventEmitted) { self::fail('Access token issued event is not emitted.'); } } } ================================================ FILE: tests/Grant/DeviceCodeGrantTest.php ================================================ getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $this::assertEquals('urn:ietf:params:oauth:grant-type:device_code', $grant->getIdentifier()); } public function testCanRespondToDeviceAuthorizationRequest(): void { $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar' ); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'scope' => 'basic', ]); $this::assertTrue($grant->canRespondToDeviceAuthorizationRequest($request)); } public function testRespondToDeviceAuthorizationRequest(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $deviceCodeRepository = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeRepository->method('getNewDeviceCode')->willReturn(new DeviceCodeEntity()); $scope = new ScopeEntity(); $scope->setIdentifier('basic'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'scope' => 'basic', ]); $deviceCodeResponse = $grant->respondToDeviceAuthorizationRequest($request); $responseJson = json_decode($deviceCodeResponse->generateHttpResponse(new Response())->getBody()->__toString()); self::assertObjectHasProperty('device_code', $responseJson); self::assertObjectHasProperty('user_code', $responseJson); self::assertObjectHasProperty('verification_uri', $responseJson); self::assertEquals('http://foo/bar', $responseJson->verification_uri); } public function testRespondToDeviceAuthorizationRequestWithVerificationUriComplete(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $deviceCodeRepository = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeRepository->method('getNewDeviceCode')->willReturn(new DeviceCodeEntity()); $scope = new ScopeEntity(); $scope->setIdentifier('basic'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setIncludeVerificationUriComplete(true); $grant->setClientRepository($clientRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'scope' => 'basic', ]); $deviceCodeResponse = $grant->respondToDeviceAuthorizationRequest($request); $responseJson = json_decode($deviceCodeResponse->generateHttpResponse(new Response())->getBody()->__toString()); self::assertObjectHasProperty('device_code', $responseJson); self::assertObjectHasProperty('user_code', $responseJson); self::assertObjectHasProperty('verification_uri', $responseJson); self::assertObjectHasProperty('verification_uri_complete', $responseJson); self::assertEquals('http://foo/bar', $responseJson->verification_uri); self::assertEquals('http://foo/bar?user_code=' . $responseJson->user_code, $responseJson->verification_uri_complete); } public function testValidateDeviceAuthorizationRequestMissingClient(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withParsedBody([ 'scope' => 'basic', ]); $this->expectException(OAuthServerException::class); $grant->respondToDeviceAuthorizationRequest($request); } public function testValidateDeviceAuthorizationRequestEmptyScope(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $request = (new ServerRequest())->withParsedBody([ 'scope' => '', ]); $this->expectException(OAuthServerException::class); $grant->respondToDeviceAuthorizationRequest($request); } public function testValidateDeviceAuthorizationRequestClientMismatch(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn(null); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'bar', 'scope' => 'basic', ]); $this->expectException(OAuthServerException::class); $grant->respondToDeviceAuthorizationRequest($request); } public function testCompleteDeviceAuthorizationRequest(): void { $deviceCode = new DeviceCodeEntity(); $deviceCode->setIdentifier('deviceCodeEntityIdentifier'); $deviceCode->setUserCode('foo'); $deviceCodeRepository = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeRepository->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCode); $grant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar', ); $grant->completeDeviceAuthorizationRequest($deviceCode->getIdentifier(), 'userId', true); $this::assertEquals('userId', $deviceCode->getUserIdentifier()); } public function testDeviceAuthorizationResponse(): void { $client = new ClientEntity(); $client->setIdentifier('clientId'); $client->setConfidential(); $client->setRedirectUri('http://foo/bar'); $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepository->method('getClientEntity')->willReturn($client); $scopeEntity = new ScopeEntity(); $scopeEntity->setIdentifier('basic'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $deviceCodeRepository = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeRepository->method('getNewDeviceCode')->willReturn(new DeviceCodeEntity()); $server = new AuthorizationServer( $clientRepository, $accessRepositoryMock, $scopeRepositoryMock, 'file://' . __DIR__ . '/../Stubs/private.key', base64_encode(random_bytes(36)), new StubResponseType() ); $server->setDefaultScope(self::DEFAULT_SCOPE); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', ]); $deviceCodeGrant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar' ); $server->enableGrantType($deviceCodeGrant); $response = $server->respondToDeviceAuthorizationRequest($serverRequest, new Response()); $responseObject = json_decode($response->getBody()->__toString()); $this::assertObjectHasProperty('device_code', $responseObject); $this::assertObjectHasProperty('user_code', $responseObject); $this::assertObjectHasProperty('verification_uri', $responseObject); $this::assertObjectHasProperty('expires_in', $responseObject); } public function testRespondToAccessTokenRequest(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scope = new ScopeEntity(); $scope->setIdentifier('foo'); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeEntity = new DeviceCodeEntity(); $deviceCodeEntity->setUserIdentifier('baz'); $deviceCodeEntity->setIdentifier('deviceCodeEntityIdentifier'); $deviceCodeEntity->setUserCode('123456'); $deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('+1 hour')); $deviceCodeEntity->setClient($client); $deviceCodeEntity->addScope($scope); $deviceCodeRepositoryMock->expects(self::atLeast(1))->method('getDeviceCodeEntityByDeviceCode') ->with($deviceCodeEntity->getIdentifier()) ->willReturn($deviceCodeEntity); $accessTokenEntity = new AccessTokenEntity(); $accessTokenEntity->setClient($client); $accessTokenEntity->addScope($scope); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->expects(self::once())->method('getNewToken') ->with($client, $deviceCodeEntity->getScopes(), $deviceCodeEntity->getUserIdentifier()) ->willReturn($accessTokenEntity); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier'); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->completeDeviceAuthorizationRequest($deviceCodeEntity->getIdentifier(), 'baz', true); $accessTokenEventEmitted = false; $refreshTokenEventEmitted = false; $grant->getListenerRegistry()->subscribeTo( RequestEvent::ACCESS_TOKEN_ISSUED, function ($event) use (&$accessTokenEventEmitted): void { self::assertInstanceOf(RequestAccessTokenEvent::class, $event); $accessTokenEventEmitted = true; } ); $grant->getListenerRegistry()->subscribeTo( RequestEvent::REFRESH_TOKEN_ISSUED, function ($event) use (&$refreshTokenEventEmitted): void { self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); $refreshTokenEventEmitted = true; } ); $serverRequest = (new ServerRequest())->withParsedBody([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 'device_code' => $deviceCodeEntity->getIdentifier(), 'client_id' => 'foo', ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); $this::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); $this::assertSame([$scope], $responseType->getAccessToken()->getScopes()); if (!$accessTokenEventEmitted) { self::fail('Access token issued event is not emitted.'); } if (!$refreshTokenEventEmitted) { self::fail('Refresh token issued event is not emitted.'); } } public function testRespondToRequestMissingClient(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn(null); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $serverRequest = (new ServerRequest())->withQueryParams([ 'device_code' => uniqid(), ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestMissingDeviceCode(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeEntity = new DeviceCodeEntity(); $deviceCodeEntity->setUserIdentifier('baz'); $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier'); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testIssueSlowDownError(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeEntity = new DeviceCodeEntity(); $deviceCodeEntity->setLastPolledAt(new DateTimeImmutable()); $deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('+1 hour')); $deviceCodeEntity->setClient($client); $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier'); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'device_code' => uniqid(), ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(13); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testIssueAuthorizationPendingError(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeEntity = new DeviceCodeEntity(); $deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('+1 hour')); $deviceCodeEntity->setClient($client); $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier'); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'device_code' => uniqid(), ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(12); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testIssueExpiredTokenError(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeEntity = new DeviceCodeEntity(); $deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('-1 hour')); $deviceCodeEntity->setClient($client); $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier'); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'device_code' => uniqid(), ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(11); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testSettingDeviceCodeIntervalRate(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $deviceCode = new DeviceCodeEntity(); $deviceCodeRepository = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeRepository->method('getNewDeviceCode')->willReturn($deviceCode); $scope = new ScopeEntity(); $scope->setIdentifier('basic'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar', self::INTERVAL_RATE ); $grant->setClientRepository($clientRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $grant->setIntervalVisibility(true); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'scope' => 'basic', ]); $deviceCodeResponse = $grant ->respondToDeviceAuthorizationRequest($request) ->generateHttpResponse(new Response()); $deviceCode = json_decode((string) $deviceCodeResponse->getBody()); $this::assertObjectHasProperty('interval', $deviceCode); $this::assertEquals(self::INTERVAL_RATE, $deviceCode->interval); } public function testSettingInternalVisibility(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $deviceCode = new DeviceCodeEntity(); $deviceCodeRepository = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeRepository->method('getNewDeviceCode')->willReturn($deviceCode); $scope = new ScopeEntity(); $scope->setIdentifier('basic'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M'), 'http://foo/bar', ); $grant->setClientRepository($clientRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $grant->setIntervalVisibility(true); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'scope' => 'basic', ]); $deviceCodeResponse = $grant ->respondToDeviceAuthorizationRequest($request) ->generateHttpResponse(new Response()); $deviceCode = json_decode((string) $deviceCodeResponse->getBody()); $this::assertObjectHasProperty('interval', $deviceCode); $this::assertEquals(5, $deviceCode->interval); } public function testIssueAccessDeniedError(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCode = new DeviceCodeEntity(); $deviceCode->setIdentifier('deviceCodeEntityIdentifier'); $deviceCode->setExpiryDateTime(new DateTimeImmutable('+1 hour')); $deviceCode->setClient($client); $deviceCode->setUserCode('12345678'); $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCode); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier'); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->completeDeviceAuthorizationRequest($deviceCode->getIdentifier(), '1', false); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'device_code' => $deviceCode->getIdentifier(), ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(9); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } } ================================================ FILE: tests/Grant/ImplicitGrantTest.php ================================================ cryptStub = new CryptTraitStub(); } public function testGetIdentifier(): void { $grant = new ImplicitGrant(new DateInterval('PT10M')); self::assertEquals('implicit', $grant->getIdentifier()); } public function testCanRespondToAccessTokenRequest(): void { $grant = new ImplicitGrant(new DateInterval('PT10M')); self::assertFalse( $grant->canRespondToAccessTokenRequest(new ServerRequest()) ); } public function testRespondToAccessTokenRequest(): void { $grant = new ImplicitGrant(new DateInterval('PT10M')); $this->expectException(LogicException::class); $grant->respondToAccessTokenRequest( new ServerRequest(), new StubResponseType(), new DateInterval('PT10M') ); } public function testCanRespondToAuthorizationRequest(): void { $grant = new ImplicitGrant(new DateInterval('PT10M')); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'token', 'client_id' => 'foo', ]); self::assertTrue($grant->canRespondToAuthorizationRequest($request)); } public function testValidateAuthorizationRequest(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'token', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, ]); self::assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request)); } public function testValidateAuthorizationRequestRedirectUriArray(): void { $client = new ClientEntity(); $client->setRedirectUri([self::REDIRECT_URI]); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'token', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, ]); self::assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request)); } public function testValidateAuthorizationRequestMissingClientId(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams(['response_type' => 'token']); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestInvalidClientId(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn(null); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'token', 'client_id' => 'foo', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(4); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestBadRedirectUriString(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'token', 'client_id' => 'foo', 'redirect_uri' => 'http://bar', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(4); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestBadRedirectUriArray(): void { $client = new ClientEntity(); $client->setRedirectUri([self::REDIRECT_URI]); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'token', 'client_id' => 'foo', 'redirect_uri' => 'http://bar', ]); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(4); $grant->validateAuthorizationRequest($request); } public function testValidateAuthorizationRequestInvalidScopes(): void { $client = new ClientEntity(); $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(null); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'token', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'scope' => 'foo', 'state' => 'foo', ]); try { $grant->validateAuthorizationRequest($request); } catch (OAuthServerException $e) { self::assertSame(5, $e->getCode()); self::assertSame('invalid_scope', $e->getErrorType()); self::assertSame('https://foo/bar#state=foo', $e->getRedirectUri()); return; } self::fail('Did not throw expected exception'); } public function testCompleteAuthorizationRequest(): void { $client = new ClientEntity(); $client->setIdentifier('identifier'); $client->setRedirectUri('https://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessToken->setUserIdentifier('userId'); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $accessTokenEventEmitted = false; $grant->getListenerRegistry()->subscribeTo( RequestEvent::ACCESS_TOKEN_ISSUED, function ($event) use (&$accessTokenEventEmitted): void { self::assertInstanceOf(RequestAccessTokenEvent::class, $event); $accessTokenEventEmitted = true; } ); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); if (!$accessTokenEventEmitted) { // self::fail('Access token issued event is not emitted.'); // TODO: next major release } } public function testCompleteAuthorizationRequestDenied(): void { $client = new ClientEntity(); $client->setIdentifier('clientId'); $client->setRedirectUri('https://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(false); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $authRequest->setState('foo'); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); try { $grant->completeAuthorizationRequest($authRequest); } catch (OAuthServerException $e) { self::assertSame(9, $e->getCode()); self::assertSame('access_denied', $e->getErrorType()); self::assertSame('https://foo/bar#state=foo', $e->getRedirectUri()); return; } self::fail('Did not throw expected exception'); } public function testAccessTokenRepositoryUniqueConstraintCheck(): void { $client = new ClientEntity(); $client->setIdentifier('clientId'); $client->setRedirectUri('https://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessToken->setUserIdentifier('userId'); /** @var AccessTokenRepositoryInterface|MockObject $accessTokenRepositoryMock */ $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock ->expects(self::exactly(2)) ->method('persistNewAccessToken') ->willReturnCallback(function (): void { static $count = 0; if (1 === ++$count) { throw UniqueTokenIdentifierConstraintViolationException::create(); } }); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } public function testAccessTokenRepositoryFailToPersist(): void { $client = new ClientEntity(); $client->setRedirectUri('https://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); /** @var AccessTokenRepositoryInterface|MockObject $accessTokenRepositoryMock */ $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(OAuthServerException::serverError('something bad happened')); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(7); $grant->completeAuthorizationRequest($authRequest); } public function testAccessTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): void { $client = new ClientEntity(); $client->setRedirectUri('https://foo/bar'); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); /** @var AccessTokenRepositoryInterface|MockObject $accessTokenRepositoryMock */ $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $this->expectException(UniqueTokenIdentifierConstraintViolationException::class); $this->expectExceptionCode(100); $grant->completeAuthorizationRequest($authRequest); } public function testSetRefreshTokenTTL(): void { $grant = new ImplicitGrant(new DateInterval('PT10M')); $this->expectException(LogicException::class); $grant->setRefreshTokenTTL(new DateInterval('PT10M')); } public function testSetRefreshTokenRepository(): void { $grant = new ImplicitGrant(new DateInterval('PT10M')); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $this->expectException(LogicException::class); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); } public function testCompleteAuthorizationRequestNoUser(): void { $grant = new ImplicitGrant(new DateInterval('PT10M')); $this->expectException(LogicException::class); $grant->completeAuthorizationRequest(new AuthorizationRequest()); } } ================================================ FILE: tests/Grant/PasswordGrantTest.php ================================================ getMockBuilder(UserRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); self::assertEquals('password', $grant->getIdentifier()); } public function testRespondToRequest(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); $userEntity = new UserEntity(); $userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn($userEntity); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $accessTokenEventEmitted = false; $refreshTokenEventEmitted = false; $grant->getListenerRegistry()->subscribeTo( RequestEvent::ACCESS_TOKEN_ISSUED, function ($event) use (&$accessTokenEventEmitted): void { self::assertInstanceOf(RequestAccessTokenEvent::class, $event); $accessTokenEventEmitted = true; } ); $grant->getListenerRegistry()->subscribeTo( RequestEvent::REFRESH_TOKEN_ISSUED, function ($event) use (&$refreshTokenEventEmitted): void { self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); $refreshTokenEventEmitted = true; } ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'username' => 'foo', 'password' => 'bar', ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); if (!$accessTokenEventEmitted) { self::fail('Access token issued event is not emitted.'); } if (!$refreshTokenEventEmitted) { self::fail('Refresh token issued event is not emitted.'); } } public function testRespondToRequestNullRefreshToken(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); $userEntity = new UserEntity(); $userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn($userEntity); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'username' => 'foo', 'password' => 'bar', ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertNull($responseType->getRefreshToken()); } public function testRespondToRequestMissingUsername(): void { $client = new ClientEntity(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $serverRequest = (new ServerRequest())->withQueryParams([ 'client_id' => 'foo', 'client_secret' => 'bar', ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestMissingPassword(): void { $client = new ClientEntity(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'username' => 'alex', ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestBadCredentials(): void { $client = new ClientEntity(); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); $userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn(null); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setScopeRepository($scopeRepositoryMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'username' => 'alex', 'password' => 'whisky', ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(6); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } } ================================================ FILE: tests/Grant/RefreshTokenGrantTest.php ================================================ cryptStub = new CryptTraitStub(); } public function testGetIdentifier(): void { $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); self::assertEquals('refresh_token', $grant->getIdentifier()); } public function testRespondToRequest(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeEntity = new ScopeEntity(); $scopeEntity->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->expects(self::once())->method('persistNewRefreshToken')->willReturnSelf(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); $accessTokenEventEmitted = false; $refreshTokenEventEmitted = false; $grant->getListenerRegistry()->subscribeTo( RequestEvent::ACCESS_TOKEN_ISSUED, function ($event) use (&$accessTokenEventEmitted): void { self::assertInstanceOf(RequestAccessTokenEvent::class, $event); $accessTokenEventEmitted = true; } ); $grant->getListenerRegistry()->subscribeTo( RequestEvent::REFRESH_TOKEN_ISSUED, function ($event) use (&$refreshTokenEventEmitted): void { self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); $refreshTokenEventEmitted = true; } ); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => '123', 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scopes' => ['foo'], ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); if (!$accessTokenEventEmitted) { self::fail('Access token issued event is not emitted.'); } if (!$refreshTokenEventEmitted) { self::fail('Refresh token issued event is not emitted.'); } } public function testRespondToRequestNullRefreshToken(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeEntity = new ScopeEntity(); $scopeEntity->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); $refreshTokenRepositoryMock->expects(self::never())->method('persistNewRefreshToken'); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => '123', 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scopes' => ['foo'], ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertNull($responseType->getRefreshToken()); } public function testRespondToReducedScopes(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $scope = new ScopeEntity(); $scope->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scope]); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo', 'bar'], 'user_id' => '123', 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scope' => 'foo', ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); } public function testRespondToUnexpectedScope(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $scope = new ScopeEntity(); $scope->setIdentifier('foobar'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo', 'bar'], 'user_id' => 123, 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scope' => 'foobar', ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(5); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestMissingOldToken(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(3); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestInvalidOldToken(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $oldRefreshToken = 'foobar'; $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $oldRefreshToken, ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(8); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestClientMismatch(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $oldRefreshToken = json_encode( [ 'client_id' => 'bar', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => 123, 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(8); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestExpiredToken(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => 123, 'expire_time' => time() - 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(8); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestRevokedToken(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('isRefreshTokenRevoked')->willReturn(true); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => 123, 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, ]); $responseType = new StubResponseType(); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(8); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRespondToRequestFinalizeScopes(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $fooScopeEntity = new ScopeEntity(); $fooScopeEntity->setIdentifier('foo'); $barScopeEntity = new ScopeEntity(); $barScopeEntity->setIdentifier('bar'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($fooScopeEntity, $barScopeEntity); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $scopes = [$fooScopeEntity, $barScopeEntity]; $finalizedScopes = [$fooScopeEntity]; $scopeRepositoryMock ->expects(self::once()) ->method('finalizeScopes') ->with($scopes, $grant->getIdentifier(), $client, '123', null) ->willReturn($finalizedScopes); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock ->expects(self::once()) ->method('getNewToken') ->with($client, $finalizedScopes) ->willReturn($accessToken); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo', 'bar'], 'user_id' => '123', 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scope' => 'foo bar', ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } public function testRevokedRefreshToken(): void { $refreshTokenId = 'foo'; $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeEntity = new ScopeEntity(); $scopeEntity->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('isRefreshTokenRevoked') ->willReturn(false, true); $refreshTokenRepositoryMock->expects(self::once())->method('revokeRefreshToken')->with(self::equalTo($refreshTokenId)); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => $refreshTokenId, 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => '123', 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scope' => 'foo', ]); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); $grant->respondToAccessTokenRequest($serverRequest, new StubResponseType(), new DateInterval('PT5M')); self::assertTrue($refreshTokenRepositoryMock->isRefreshTokenRevoked($refreshTokenId)); } public function testUnrevokedRefreshToken(): void { $refreshTokenId = 'foo'; $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeEntity = new ScopeEntity(); $scopeEntity->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); $accessTokenEntity = new AccessTokenEntity(); $accessTokenEntity->setClient($client); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessTokenEntity); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('isRefreshTokenRevoked')->willReturn(false); $refreshTokenRepositoryMock->expects(self::never())->method('revokeRefreshToken'); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => $refreshTokenId, 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => '123', 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scope' => 'foo', ]); $privateKey = new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey($privateKey); $grant->revokeRefreshTokens(false); $responseType = new BearerTokenResponse(); $responseType->setPrivateKey($privateKey); $responseType->setEncryptionKey($this->cryptStub->getKey()); $response = $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')) ->generateHttpResponse(new Response()); $json = json_decode((string) $response->getBody()); self::assertFalse($refreshTokenRepositoryMock->isRefreshTokenRevoked($refreshTokenId)); self::assertEquals('Bearer', $json->token_type); self::assertObjectHasProperty('expires_in', $json); self::assertObjectHasProperty('access_token', $json); self::assertObjectHasProperty('refresh_token', $json); self::assertNotSame($json->refresh_token, $encryptedOldRefreshToken); } public function testRespondToRequestWithIntUserId(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeEntity = new ScopeEntity(); $scopeEntity->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenEntity = new AccessTokenEntity(); $accessTokenEntity->setClient($client); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessTokenEntity); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->expects(self::once())->method('persistNewRefreshToken')->willReturnSelf(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); $oldRefreshToken = json_encode( [ 'client_id' => 'foo', 'refresh_token_id' => 'zyxwvu', 'access_token_id' => 'abcdef', 'scopes' => ['foo'], 'user_id' => 123, 'expire_time' => time() + 3600, ] ); if ($oldRefreshToken === false) { self::fail('json_encode failed'); } $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( $oldRefreshToken ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', 'refresh_token' => $encryptedOldRefreshToken, 'scopes' => ['foo'], ]); $responseType = new StubResponseType(); $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); } } ================================================ FILE: tests/Middleware/AuthorizationServerMiddlewareTest.php ================================================ setConfidential(); $client->setRedirectUri('http://foo/bar'); $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepository->method('getClientEntity')->willReturn($client); $clientRepository->method('validateClient')->willReturn(true); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $server = new AuthorizationServer( $clientRepository, $accessRepositoryMock, $scopeRepositoryMock, 'file://' . __DIR__ . '/../Stubs/private.key', base64_encode(random_bytes(36)), new StubResponseType() ); $server->setDefaultScope(self::DEFAULT_SCOPE); $server->enableGrantType(new ClientCredentialsGrant()); $_POST['grant_type'] = 'client_credentials'; $_POST['client_id'] = 'foo'; $_POST['client_secret'] = 'bar'; $request = ServerRequestFactory::fromGlobals(); $middleware = new AuthorizationServerMiddleware($server); $response = $middleware->__invoke( $request, new Response(), function () { return func_get_args()[1]; } ); self::assertEquals(200, $response->getStatusCode()); } public function testOAuthErrorResponse(): void { $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepository->method('validateClient')->willReturn(false); $server = new AuthorizationServer( $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/../Stubs/private.key', base64_encode(random_bytes(36)), new StubResponseType() ); $server->enableGrantType(new ClientCredentialsGrant(), new DateInterval('PT1M')); $_POST['grant_type'] = 'client_credentials'; $_POST['client_id'] = 'foo'; $_POST['client_secret'] = 'bar'; $request = ServerRequestFactory::fromGlobals(); $middleware = new AuthorizationServerMiddleware($server); $response = $middleware->__invoke( $request, new Response(), function () { return func_get_args()[1]; } ); self::assertEquals(401, $response->getStatusCode()); } public function testOAuthErrorResponseRedirectUri(): void { $exception = OAuthServerException::invalidScope('test', 'http://foo/bar'); $response = $exception->generateHttpResponse(new Response()); self::assertEquals(302, $response->getStatusCode()); self::assertEquals( 'http://foo/bar?error=invalid_scope&error_description=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope', $response->getHeader('location')[0] ); } public function testOAuthErrorResponseRedirectUriFragment(): void { $exception = OAuthServerException::invalidScope('test', 'http://foo/bar'); $response = $exception->generateHttpResponse(new Response(), true); self::assertEquals(302, $response->getStatusCode()); self::assertEquals( 'http://foo/bar#error=invalid_scope&error_description=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope', $response->getHeader('location')[0] ); } } ================================================ FILE: tests/Middleware/ResourceServerMiddlewareTest.php ================================================ getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/../Stubs/public.key' ); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('test'); $accessToken->setUserIdentifier('123'); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $token = $accessToken->toString(); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $token)); $middleware = new ResourceServerMiddleware($server); $response = $middleware->__invoke( $request, new Response(), function () { self::assertEquals('test', func_get_args()[0]->getAttribute('oauth_access_token_id')); return func_get_args()[1]; } ); self::assertEquals(200, $response->getStatusCode()); } public function testValidResponseExpiredToken(): void { $server = new ResourceServer( $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/../Stubs/public.key' ); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('test'); $accessToken->setUserIdentifier('123'); $accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $token = $accessToken->toString(); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $token)); $middleware = new ResourceServerMiddleware($server); $response = $middleware->__invoke( $request, new Response(), function () { self::assertEquals('test', func_get_args()[0]->getAttribute('oauth_access_token_id')); return func_get_args()[1]; } ); self::assertEquals(401, $response->getStatusCode()); } public function testErrorResponse(): void { $server = new ResourceServer( $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/../Stubs/public.key' ); $request = (new ServerRequest())->withHeader('authorization', ''); $middleware = new ResourceServerMiddleware($server); $response = $middleware->__invoke( $request, new Response(), function () { return func_get_args()[1]; } ); self::assertEquals(401, $response->getStatusCode()); } } ================================================ FILE: tests/PHPStan/AbstractGrantExtension.php ================================================ getName(), [ 'getRequestParameter', 'getQueryStringParameter', 'getCookieParameter', ], true); } public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { return TypeCombinator::union(...[ new StringType(), property_exists($methodCall, 'getArgs') && isset($methodCall->getArgs[2]) ? $scope->getType($methodCall->getArgs[2]->value) : new NullType(), ]); } } ================================================ FILE: tests/RedirectUriValidators/RedirectUriValidatorTest.php ================================================ validateRedirectUri($invalidRedirectUri), 'Non loopback URI must match in every part' ); } public function testValidNonLoopbackUri(): void { $validator = new RedirectUriValidator([ 'https://example.com:8443/endpoint', 'https://example.com/different/endpoint', ]); $validRedirectUri = 'https://example.com:8443/endpoint'; self::assertTrue( $validator->validateRedirectUri($validRedirectUri), 'Redirect URI must be valid when matching in every part' ); } public function testInvalidLoopbackUri(): void { $validator = new RedirectUriValidator('http://127.0.0.1:8443/endpoint'); $invalidRedirectUri = 'http://127.0.0.1:8443/different/endpoint'; self::assertFalse( $validator->validateRedirectUri($invalidRedirectUri), 'Valid loopback redirect URI can change only the port number' ); } public function testValidLoopbackUri(): void { $validator = new RedirectUriValidator('http://127.0.0.1:8443/endpoint'); $validRedirectUri = 'http://127.0.0.1:8080/endpoint'; self::assertTrue( $validator->validateRedirectUri($validRedirectUri), 'Loopback redirect URI can change the port number' ); } public function testValidIpv6LoopbackUri(): void { $validator = new RedirectUriValidator('http://[::1]:8443/endpoint'); $validRedirectUri = 'http://[::1]:8080/endpoint'; self::assertTrue( $validator->validateRedirectUri($validRedirectUri), 'Loopback redirect URI can change the port number' ); } public function testCanValidateUrn(): void { $validator = new RedirectUriValidator('urn:ietf:wg:oauth:2.0:oob'); self::assertTrue( $validator->validateRedirectUri('urn:ietf:wg:oauth:2.0:oob'), 'An invalid pre-registered urn was provided' ); } public function canValidateCustomSchemeHost(): void { $validator = new RedirectUriValidator('msal://redirect'); self::assertTrue( $validator->validateRedirectUri('msal://redirect'), 'An invalid, pre-registered, custom scheme uri was provided' ); } public function canValidateCustomSchemePath(): void { $validator = new RedirectUriValidator('com.example.app:/oauth2redirect/example-provider'); self::assertTrue( $validator->validateRedirectUri('com.example.app:/oauth2redirect/example-provider'), 'An invalid, pre-registered, custom scheme uri was provided' ); } } ================================================ FILE: tests/ResourceServerTest.php ================================================ getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), 'file://' . __DIR__ . '/Stubs/public.key' ); try { $server->validateAuthenticatedRequest(ServerRequestFactory::fromGlobals()); } catch (OAuthServerException $e) { self::assertEquals('Missing "Authorization" header', $e->getHint()); } } } ================================================ FILE: tests/ResponseTypes/BearerResponseTypeTest.php ================================================ setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $scope = new ScopeEntity(); $scope->setIdentifier('basic'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->addScope($scope); $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $accessToken->setUserIdentifier('userId'); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); $refreshToken->setAccessToken($accessToken); $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $responseType->setAccessToken($accessToken); $responseType->setRefreshToken($refreshToken); $response = $responseType->generateHttpResponse(new Response()); self::assertEquals(200, $response->getStatusCode()); self::assertEquals('no-cache', $response->getHeader('pragma')[0]); self::assertEquals('no-store', $response->getHeader('cache-control')[0]); self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); $response->getBody()->rewind(); $json = json_decode($response->getBody()->getContents()); self::assertEquals('Bearer', $json->token_type); self::assertObjectHasProperty('expires_in', $json); self::assertObjectHasProperty('access_token', $json); self::assertObjectHasProperty('refresh_token', $json); } public function testGenerateHttpResponseWithExtraParams(): void { $responseType = new BearerTokenResponseWithParams(); $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $scope = new ScopeEntity(); $scope->setIdentifier('basic'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->addScope($scope); $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $accessToken->setUserIdentifier('userId'); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); $refreshToken->setAccessToken($accessToken); $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $responseType->setAccessToken($accessToken); $responseType->setRefreshToken($refreshToken); $response = $responseType->generateHttpResponse(new Response()); self::assertEquals(200, $response->getStatusCode()); self::assertEquals('no-cache', $response->getHeader('pragma')[0]); self::assertEquals('no-store', $response->getHeader('cache-control')[0]); self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); $response->getBody()->rewind(); $json = json_decode($response->getBody()->getContents()); self::assertEquals('Bearer', $json->token_type); self::assertObjectHasProperty('expires_in', $json); self::assertObjectHasProperty('access_token', $json); self::assertObjectHasProperty('refresh_token', $json); self::assertObjectHasProperty('foo', $json); self::assertEquals('bar', $json->foo); } public function testDetermineAccessTokenInHeaderValidToken(): void { $responseType = new BearerTokenResponse(); $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); $accessToken->setUserIdentifier('123'); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); $refreshToken->setAccessToken($accessToken); $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $responseType->setAccessToken($accessToken); $responseType->setRefreshToken($refreshToken); $response = $responseType->generateHttpResponse(new Response()); $json = json_decode((string) $response->getBody()); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $json->access_token)); $request = $authorizationValidator->validateAuthorization($request); self::assertEquals('abcdef', $request->getAttribute('oauth_access_token_id')); self::assertEquals('clientName', $request->getAttribute('oauth_client_id')); self::assertEquals('123', $request->getAttribute('oauth_user_id')); self::assertEquals([], $request->getAttribute('oauth_scopes')); } public function testDetermineAccessTokenInHeaderInvalidJWT(): void { $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $responseType = new BearerTokenResponse(); $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); $accessToken->setUserIdentifier('123'); $accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); $refreshToken->setAccessToken($accessToken); $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $responseType->setAccessToken($accessToken); $responseType->setRefreshToken($refreshToken); $response = $responseType->generateHttpResponse(new Response()); $json = json_decode((string) $response->getBody()); $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $json->access_token)); try { $authorizationValidator->validateAuthorization($request); } catch (OAuthServerException $e) { self::assertEquals( 'Access token could not be verified', $e->getHint() ); } } public function testDetermineAccessTokenInHeaderRevokedToken(): void { $responseType = new BearerTokenResponse(); $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); $accessToken->setUserIdentifier('123'); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); $refreshToken->setAccessToken($accessToken); $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $responseType->setAccessToken($accessToken); $responseType->setRefreshToken($refreshToken); $response = $responseType->generateHttpResponse(new Response()); $json = json_decode((string) $response->getBody()); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(true); $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $json->access_token)); try { $authorizationValidator->validateAuthorization($request); } catch (OAuthServerException $e) { self::assertEquals( 'Access token has been revoked', $e->getHint() ); } } public function testDetermineAccessTokenInHeaderInvalidToken(): void { $responseType = new BearerTokenResponse(); $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $request = (new ServerRequest())->withHeader('authorization', 'Bearer blah'); try { $authorizationValidator->validateAuthorization($request); } catch (OAuthServerException $e) { self::assertEquals( 'The JWT string must have two dots', $e->getHint() ); } } public function testDetermineMissingBearerInHeader(): void { $responseType = new BearerTokenResponse(); $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); $request = (new ServerRequest())->withHeader('authorization', 'Bearer blah.blah.blah'); try { $authorizationValidator->validateAuthorization($request); } catch (OAuthServerException $e) { self::assertEquals( 'Error while decoding from JSON', $e->getHint() ); } } } ================================================ FILE: tests/ResponseTypes/BearerTokenResponseWithParams.php ================================================ */ protected function getExtraParams(AccessTokenEntityInterface $accessToken): array { return ['foo' => 'bar', 'token_type' => 'Should not overwrite']; } } ================================================ FILE: tests/ResponseTypes/DeviceCodeResponseTypeTest.php ================================================ setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $scope = new ScopeEntity(); $scope->setIdentifier('basic'); $deviceCode = new DeviceCodeEntity(); $deviceCode->setIdentifier('abcdef'); $deviceCode->setUserCode('12345678'); $deviceCode->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $deviceCode->setClient($client); $deviceCode->addScope($scope); $deviceCode->setVerificationUri('https://example.com/device'); $responseType->setDeviceCodeEntity($deviceCode); $response = $responseType->generateHttpResponse(new Response()); $this::assertEquals(200, $response->getStatusCode()); $this::assertEquals('no-cache', $response->getHeader('pragma')[0]); $this::assertEquals('no-store', $response->getHeader('cache-control')[0]); $this::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); $response->getBody()->rewind(); $json = json_decode($response->getBody()->getContents()); $this::assertObjectHasProperty('expires_in', $json); $this::assertObjectHasProperty('device_code', $json); $this::assertEquals('abcdef', $json->device_code); $this::assertObjectHasProperty('verification_uri', $json); $this::assertObjectHasProperty('user_code', $json); } } ================================================ FILE: tests/Stubs/.gitattributes ================================================ private.key.crlf text eol=crlf ================================================ FILE: tests/Stubs/AccessTokenEntity.php ================================================ redirectUri = $uri; } public function setConfidential(): void { $this->isConfidential = true; } } ================================================ FILE: tests/Stubs/CryptTraitStub.php ================================================ setEncryptionKey(base64_encode(random_bytes(36))); } public function getKey(): string|Key|null { return $this->encryptionKey; } public function doEncrypt(string $unencryptedData): string { return $this->encrypt($unencryptedData); } public function doDecrypt(string $encryptedData): string { return $this->decrypt($encryptedData); } } ================================================ FILE: tests/Stubs/DeviceCodeEntity.php ================================================ emitter = $emitter; return $this; } public function getEmitter(): EventEmitter { return $this->emitter; } public function setRefreshTokenTTL(DateInterval $refreshTokenTTL): void { } public function getIdentifier(): string { return 'grant_type_identifier'; } public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { return $responseType; } public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool { return true; } public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequest { $authRequest = new AuthorizationRequest(); $authRequest->setGrantTypeId(self::class); return $authRequest; } public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): BearerTokenResponse { return new BearerTokenResponse(); } public function canRespondToAccessTokenRequest(ServerRequestInterface $request): bool { return true; } public function setClientRepository(ClientRepositoryInterface $clientRepository): void { } public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void { } public function setScopeRepository(ScopeRepositoryInterface $scopeRepository): void { } public function setDefaultScope(string $scope): void { } public function setPrivateKey(CryptKeyInterface $privateKey): void { } public function setEncryptionKey(Key|string|null $key = null): void { } public function revokeRefreshTokens(bool $willRevoke): void { } public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request): bool { return true; } public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void { } public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse { return new DeviceCodeResponse(); } public function setIntervalVisibility(bool $intervalVisibility): void { } public function getIntervalVisibility(): bool { return false; } public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void { } } ================================================ FILE: tests/Stubs/RefreshTokenEntity.php ================================================ getIdentifier(); } } ================================================ FILE: tests/Stubs/StubResponseType.php ================================================ accessToken; } public function getRefreshToken(): RefreshTokenEntityInterface|null { return $this->refreshToken ?? null; } public function setAccessToken(AccessTokenEntityInterface $accessToken): void { $this->accessToken = $accessToken; } public function setRefreshToken(RefreshTokenEntityInterface $refreshToken): void { $this->refreshToken = $refreshToken; } /** * @throws OAuthServerException */ public function validateAccessToken(ServerRequestInterface $request): ServerRequestInterface { if ($request->getHeader('authorization')[0] === 'Basic test') { return $request->withAttribute('oauth_access_token_id', 'test'); } throw OAuthServerException::accessDenied(); } public function generateHttpResponse(ResponseInterface $response): ResponseInterface { return new Response(); } } ================================================ FILE: tests/Stubs/UserEntity.php ================================================ setIdentifier('123'); } } ================================================ FILE: tests/Stubs/private.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAwVBWI/czetM28h+oUxi3iHpchFUIvVVsTaPRfq/WypSTpL5P kwXxAgY14B3rBC6NOpVSmyIfDSxziFPFCdeWew+P1Z846Dw+VpeQvQ/3ABlCyvks +Sw+D8xkt3r0Onbb3jkwt57f3VYHw9pCsvUQSSwK2X2CWSw8WJtDumhuMk95jXhR xxSB65jRorxH2ZBFdsY5SQ/knQE0PkTA5lQIsMfrTtIyIpHgR4LOQvLxSCRP7UV3 Nf00gsWAcvzxcKTTUE/BVZL9vUGAlje+lIWU1tk3o4aFhvX7Vbf38WPSW931Btn2 RMyoyS5JBSVsBX4Lf8o2X+rR4COpKtLrMhFAwwIDAQABAoIBAG6gy/sfH83di+c6 KLkNPxMSe1mb1DmN5kH0SxKGDJS4PFoeMym/T7JJ3ZEZbgJhpZ7uD20KNNz0IFXX Ir7EKrDYMgcdbJKyzzX83O7lcQQUcL35pTwfjpC59rVt3tCKbr8Y2YFroU9oSV9S y7LgPeayFq9qfSsM/qfyiurlkpKlG9y3dv/yxdgQS0K895wFaOHXKOnbbe6ISwgF 9/z/OsIAVVVe+FqA8gY//F3Tst7B3AbhjgpR/Ke5YXRoUMUU0it2HkAh+xd1RKHq +Y71EAB2N+XPkGswwOCQPFOW1tMES66g0WePOn+Igiz6k5wvadVSP9hNlSwUZ24R MShsOXkCgYEA6G5B9fNbCFpAWGdNYC9R8rlRK6HPrMvU6mf6+CikLlNxZSCpLddt 73nJRjjaO/addD/t3keDl/CflALGs2+hDEJP3rHe8pUH1oR5Wfnojx65R+NTCVQF ZfiZ3C50ySXd9Ffig/BuOqIB+/8zyGOfgsUM7pCYxJwlqU8NsdPiHrUCgYEA1Oqh nR6SCzzCh6sdXLFUFZmdamwMeTHGwh5OiOgfBNi2gXH8VlX8Bt61B24IL2hcXMf/ 5o4N4X/VmotaR+7HdOXgM73L7OVDGo53dNtGvrwPzEixT0DH/z4ifcfQIgmFxqGE NQZd20VsvO2cnRFK8o0MFS3vrP9XdE87DHv/FJcCgYEArGFhgCR9JkOxJx9uLmDJ +SdhwOdgG3qMrVByvGt/4G+4UNUZQ8tbWFlNYkw15nTvr9Dd/JWzThOCdoZckaW/ nlTr4XCvtd+7kWhsi8Ohq8uQhHVfFzL+UfM/QSIfMTNpWpd3gnzlc4zFxfwujnb8 TUMRZTlOY7qe3+Omd3V6ZWUCgYBguFFAOaHoPuqzjJTjBZ8HzOeIb5re9zCt/+x3 HtLwda26cdhKM/cv+71Kqb5IIuVKNIRX7JH7rQGQmdsiMCMlREOr3X0kmST5jFxR lka14GJgz2jUcr4ngcdTUhCHVcIScE7Jc6HxOMFjtaDebPuZ4V7qxBpLgRbuPAu7 6Rv/8wKBgQC0pj+WA/NqtBMgy+oHPBy8G+oCW8WWh7T3LlEtk85wR+LU9gRX2fSM snXGf2QUB2Fpfipl1HYbcbCZMhWT163tjnqk2dCkz9Ym9LUFE4uM9TN0jbbR4ioQ b4wGF3CC+2SL5BcjbaPOqzT6v2NOulaInvhnG5d4CwF36TyVmfj/6g== -----END RSA PRIVATE KEY----- ================================================ FILE: tests/Stubs/private.key.crlf ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAtHYxRBYATiiyDFs3pEhFg6Ei/UiQEmolTaQyQK810xHY23+X 4elLl6HP1J09mefmJ3ZdIgjIOS6rfK1BQnZIvI+IkoC7+qpD92y9f48iL0tCYKsn i1LFFjP0bESTGDe7XANifQPkp9GvKgJbu7h1/ac8x4CBSU0ZjtEvinQRsdYil6OM MXLWGozbBy13X8G+Ganv2i1aPZ2B25GyrH6lVIEwztGrSYxUrFVL+8dHhONf6PYX 19gjdzxkXCYQy2AGMc1FevZmnpIqDNQwX7CUUXQ4TDJmiP0aBEni094gUhnRFUr9 dmGpLQcCb2i0WMh2K+swFk3EutDAJ+73LKoZ3QIDAQABAoIBADo8Tge3xd9zGIoO QbV9MRmaPW1ZJk0a/fDBRQpEwGzdvIqQ8VWQ8Lj9GdF18LQi9s3TT5i1FtAFNIfm bUHiY/SdqSgF7SOmIIrPB5QLf6+dbM0/TmKSklFo8L6jnohZK9g0q2rGf9p8Ozem TS4WB9WUS3PiD1a1T8Mb1Gisri0h7rvI4TIkrcx6lUUCgphCZd2TWUhmE3YmybOg 4h855W685g/ydzjwB+5Y6CS3V6a78Z5Gb4df3l0XfqCWh/xzuNs7nIpRv8CE0vRE vq9j/cVyKkzMjiagteJaisTCBkDmtAi9dEVL8uaSDoTJq1g+VOGuJxHUm31Pavqr 3RwvXS0CgYEA74jUqmzxAwr/uBWquIkfMg+hsKjJe3gsSAJIAPzcA9OkzZd9w/1R P8C92N2UaDbCW7ZEl7ZzS+IO6nA4OcR98j77/nBk6cYykyVRkSaj01epz3bRApxc R18e49MBftSMnI5R7lIJO/UAIRfd0rntX4jkdVAdn9s/VOvG8w4KQXcCgYEAwN3W b3azSNYlj4CW8+t6qS/3JQ/qpPgVuqkqP9dQXC9O6VlV03pJIwFk2Ldjd7/eXT+0 hFVB3O71iECfet/1UgustlgFp5I4ZrPmYF/J1nGpx1KIE8P4d0qC8lODtdnsGAcU +/vBjXinX7pWgM8e6LAJzqNUq/xal/wNY325dEsCgYB7J0+n+/ECToJhhApNbHq0 g2LvcCh/Ka8iqsGYeGkqMoOWDKBlxvUiIRe6y1nFJvpQquqjUfP/fM+Ma3wM/2B9 zzJChEjuBK/2BYblaQdr3rN47i7R99BeBaLdIZywN9m/mFC5hkYnJHUXjqzG7j8E El7bjgBdMx1hrQOR7ZMKSwKBgQC2SXXBiBlPwEdj6I/EH06h1hnrR63pGim/cN/j 0ye62WPmHW+HH888bLbaNgqnRgtvayS85rAHlzst+pZBVqfRUgN9nJhLl2IDgAlA EYj9TBTBtXmz5MdUSHKXguO73yrMUvU8bOi1Q9I+IipcOGboWmoKikke/LbLa4lj /ZJpHQKBgQCuDanU+AJKgUQkkC2gHwT8quxPoRcFFErHp3iaDAwd5XsZJG9FHQUP RkPE+JkSaj65byFLhCPHUayfk4Y4udHEy4cXiv2SxZNK8q1HwuFEvb7uFprj0hNs 14qJunONVt/jzswdwO5kGVbpGlHl7U0JABnTJP71fW/rE5SH4zYxqg== -----END RSA PRIVATE KEY----- ================================================ FILE: tests/Stubs/public.key ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVBWI/czetM28h+oUxi3 iHpchFUIvVVsTaPRfq/WypSTpL5PkwXxAgY14B3rBC6NOpVSmyIfDSxziFPFCdeW ew+P1Z846Dw+VpeQvQ/3ABlCyvks+Sw+D8xkt3r0Onbb3jkwt57f3VYHw9pCsvUQ SSwK2X2CWSw8WJtDumhuMk95jXhRxxSB65jRorxH2ZBFdsY5SQ/knQE0PkTA5lQI sMfrTtIyIpHgR4LOQvLxSCRP7UV3Nf00gsWAcvzxcKTTUE/BVZL9vUGAlje+lIWU 1tk3o4aFhvX7Vbf38WPSW931Btn2RMyoyS5JBSVsBX4Lf8o2X+rR4COpKtLrMhFA wwIDAQAB -----END PUBLIC KEY----- ================================================ FILE: tests/Utils/CryptKeyTest.php ================================================ expectException(LogicException::class); new CryptKey('undefined file'); } public function testKeyCreation(): void { $keyFile = __DIR__ . '/../Stubs/public.key'; $key = new CryptKey($keyFile, 'secret'); self::assertEquals('file://' . $keyFile, $key->getKeyPath()); self::assertEquals('secret', $key->getPassPhrase()); } public function testKeyString(): void { $keyContent = file_get_contents(__DIR__ . '/../Stubs/public.key'); if (!is_string($keyContent)) { self::fail('The public key stub is not a string'); } $key = new CryptKey($keyContent); self::assertEquals( $keyContent, $key->getKeyContents() ); $keyContent = file_get_contents(__DIR__ . '/../Stubs/private.key.crlf'); if (!is_string($keyContent)) { self::fail('The private key (crlf) stub is not a string'); } $key = new CryptKey($keyContent); self::assertEquals( $keyContent, $key->getKeyContents() ); } public function testUnsupportedKeyType(): void { $this->expectException(LogicException::class); $this->expectExceptionMessage('Invalid key supplied'); try { // Create the keypair $res = openssl_pkey_new([ 'digest_alg' => 'sha512', 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_DSA, ]); if ($res === false) { self::fail('The keypair was not created'); } // Get private key openssl_pkey_export($res, $keyContent, 'mystrongpassword'); $path = self::generateKeyPath($keyContent); new CryptKey($keyContent, 'mystrongpassword'); } finally { if (isset($path)) { @unlink($path); } } } public function testECKeyType(): void { try { // Create the keypair $res = openssl_pkey_new([ 'digest_alg' => 'sha512', 'curve_name' => 'prime256v1', 'private_key_type' => OPENSSL_KEYTYPE_EC, ]); if ($res === false) { self::fail('The keypair was not created'); } // Get private key openssl_pkey_export($res, $keyContent, 'mystrongpassword'); $key = new CryptKey($keyContent, 'mystrongpassword'); self::assertEquals('', $key->getKeyPath()); self::assertEquals('mystrongpassword', $key->getPassPhrase()); } catch (Throwable $e) { self::fail('The EC key was not created'); } } public function testRSAKeyType(): void { try { // Create the keypair $res = openssl_pkey_new([ 'digest_alg' => 'sha512', 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, ]); if ($res === false) { self::fail('The keypair was not created'); } // Get private key openssl_pkey_export($res, $keyContent, 'mystrongpassword'); $key = new CryptKey($keyContent, 'mystrongpassword'); self::assertEquals('', $key->getKeyPath()); self::assertEquals('mystrongpassword', $key->getPassPhrase()); } catch (Throwable $e) { self::fail('The RSA key was not created'); } } private static function generateKeyPath(string $keyContent): string { return 'file://' . sys_get_temp_dir() . '/' . sha1($keyContent) . '.key'; } } ================================================ FILE: tests/Utils/CryptTraitTest.php ================================================ cryptStub = new CryptTraitStub(); } public function testEncryptDecryptWithPassword(): void { $this->cryptStub->setEncryptionKey(base64_encode(random_bytes(36))); $this->encryptDecrypt(); } public function testEncryptDecryptWithKey(): void { $this->cryptStub->setEncryptionKey(Key::createNewRandomKey()); $this->encryptDecrypt(); } private function encryptDecrypt(): void { $payload = 'alex loves whisky'; $encrypted = $this->cryptStub->doEncrypt($payload); $plainText = $this->cryptStub->doDecrypt($encrypted); self::assertNotEquals($payload, $encrypted); self::assertEquals($payload, $plainText); } }