diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f5b6b2a4..c1c02764 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ["8.0", "8.1", "8.2"] + php-versions: ["8.1", "8.2"] steps: - name: Setup PHP, with composer and extensions @@ -55,7 +55,7 @@ jobs: run: composer install --no-progress --prefer-dist --optimize-autoloader - name: Decide whether to run code coverage or not - if: ${{ matrix.php-versions != '8.0' }} + if: ${{ matrix.php-versions != '8.1' }} run: | echo "NO_COVERAGE=--no-coverage" >> $GITHUB_ENV @@ -65,7 +65,7 @@ jobs: ./vendor/bin/phpunit $NO_COVERAGE - name: Save coverage data - if: ${{ matrix.php-versions == '8.0' }} + if: ${{ matrix.php-versions == '8.1' }} uses: actions/upload-artifact@v1 with: name: build-data @@ -78,7 +78,7 @@ jobs: - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php with: - php-version: "8.0" + php-version: "8.1" extensions: mbstring, xml tools: composer:v2 coverage: none @@ -119,7 +119,7 @@ jobs: - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php with: - php-version: "8.0" + php-version: "8.1" extensions: mbstring, xml tools: composer:v2 coverage: none @@ -152,7 +152,7 @@ jobs: - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php with: - php-version: "8.0" + php-version: "8.1" tools: composer:v2 extensions: mbstring, xml diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 6c6e95ce..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,88 +0,0 @@ -# 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). - -## [2.0.4] -### Fixed -- Attempt fix for 'pull access denied for symfonycorp/cli' by @pradtke in #188 -- Add Access-Control-Allow-Origin header to responses, if not already present by @cicnavi in #190 - - -## [2.0.3] -### Fixed -- Use InMemory::empty by @pkoenig10 in #186 - -## [2.0.2] - 2022-07-22 -### Fixed -- Correct readme typo for module_oidc.php template path by @dgoosens in #168 -- Allow overriding cert+key name/location by @pradtke in #167 -- Fix access token timestamps, add issuer by @cicnavi in #174 -- Fix PK constraint name for allowed origin table - make it unique by @cicnavi in #173 -- Set restart url for authorize commands by @pradtke in #180 -- Fix admin-clients link by @Pyrex-FWI in #177 -- Logout tokens should have typ header with value 'logout+jwt' by @IlanaRadinsky in #185 -- Fail actions on code quality issues by @pradtke in #175 - -## [2.0.1] -### Fixed -- Make lib/Store/* available for Symfony DI. -- Fix ClientEntity postLogoutRedirectUri json_decode when 'post_logout_redirect_uri' is not defined - -## [2.0.0-rc.1] - 2021-10-08 -### Added -- Implicit flow support -- Back-channel logout -- RP initiated logout -- Support for 'sid' claim in ID and logout token -- Support for claim types -- Allow users with specific entitlements to add clients -- Support for ACR -- Support for requesting individual claims -- Support for allowed CORS origins for public clients -- Support for 'at_hash' claim in ID token -- Support for 'max_age' parameter -- List of supported grant types in OP configuration document -- List of supported auth methods for token endpoint in OP configuration document -- Support for 'prompt' parameter, for example using 'prompt=login' to require authentication -even if user has active SSO session -- Works with SSP new UI templating enabled -- Pagination for client list -- Support for basic authentication processing filters, for example for f-ticks logging, attribute -manipulation or similar, definable in oidc_config.php -- Support for 'nonce' claim in ID token -- Config options to add prefix to private scope claims and to enable multi-valued claims -### Changed -- Basic flow is now conformant -- Admin client configuration path has moved -- 'token_endpoint' renamed form '.../access_token.php' to '.../token.php' -- Requires php > 7.4 -- Auth. source is now optional when defining clients. If auth. source is not set for particular -client, a default one from the configuration will be used during authn. -### Fixed -- When authorization code is reused corresponding tokens are now immediately revoked -- Returning or displaying proper error messages is now more in line to specification -- Expired access tokens are now only deleted if corresponding refresh tokens are also expired -- JWT header parameter 'kid' is now generated dynamically based on public certificate fingerprint - -## [1.0.0-rc.2] - 2020-05-17 -### Added -- Second release candidate -- Updated league/oauth2-server to version 8.1 -### Changed -- Removed pkce config option -- New field _is_confidential_ in client (disabled for previous clients) -- Update database schema - -## [1.0.0-rc.1] - 2018-11-13 -### Added -- First release candidate -### Changed -- BC: Config file (`module_oidc.php`) has changed. Predefined scopes must be removed: openid, profile, mail, address, phone. - - -## [1.0.0-alpha.1] - 2018-04-11 -### Added -- First pre-release diff --git a/CONFORMANCE_TEST.md b/CONFORMANCE_TEST.md index 3e91c8dc..0f2fbfe5 100644 --- a/CONFORMANCE_TEST.md +++ b/CONFORMANCE_TEST.md @@ -20,7 +20,7 @@ MAVEN_CACHE=./m2 docker-compose -f builder-compose.yml run builder docker-compose up ``` -This will startup the Java conformance app and a MongoDB server. You'll need to configure a test. +This will start up the Java conformance app and a MongoDB server. You'll need to configure a test. Visit https://localhost:8443/ and "Create a new plan". The Test Plan should be "OpenID Connect Core: Basic Certification Profile Authorization server test" @@ -33,20 +33,21 @@ You'll need to get your OIDC SSP image running next ## Run SSP -You'll need to run SSP with OIDC on the same docker network as the compliance tests so they are able to communicate. +You'll need to run SSP with OIDC on the same docker network as the compliance tests, so they are able to communicate. See "Docker Compose" section of the main README. ## Run Conformance Tests -The conformance tests are interactive to make you authenticate. Some of the tests require you to clear cookies to confirm -certain test scenarios, while others require you to have session cookies to test the RP signaling to the OP that the user -should reauthenticate. The tests may also redirect you to https://localhost.emobix.co.uk:8443/ which will resolve to -the conformance Java container. You'll need to accept any SSL connection warnings. +The conformance tests are interactive to make you authenticate. Some of the tests require you to clear cookies to +confirm certain test scenarios, while others require you to have session cookies to test the RP signaling to the +OP that the user should reauthenticate. The tests may also redirect you to https://localhost.emobix.co.uk:8443/ +which will resolve to the conformance Java container. You'll need to accept any SSL connection warnings. ## Run automated tests -Eventually these test can have [the browser portion automated](https://gitlab.com/openid/conformance-suite/-/wikis/Design/BrowserControl) +Eventually these test can have +[the browser portion automated](https://gitlab.com/openid/conformance-suite/-/wikis/Design/BrowserControl) though the Conformance tests authors recommend getting them all to pass first. To run basic profile test, launch this command in console inside `simplesamlphp-module-oidc` directory: @@ -96,13 +97,13 @@ In this situation your OIDC OP must be accessible to the public internet. ## Deploy SSP OIDC Image The docker image created in the README.md is designed to be used for running the conformance tests. -It contains an sqlite database pre-populated with data that can be used for these tests. +It contains a sqlite database pre-populated with data that can be used for these tests. Build and run the image somewhere. ## Register and Create Conformance Tests Visit https://openid.net/certification/instructions/ -You can use the `json` deployment configurations under `conformance-tests` to configure your cloud instances. Update your -`discoveryUrl` to reflect the location you deployed SSP. You may also need to adjust `alias` since that is used in all -client redirect URIs and may conflict with existing test suites. +You can use the `json` deployment configurations under `conformance-tests` to configure your cloud instances. Update +your `discoveryUrl` to reflect the location you deployed SSP. You may also need to adjust `alias` since that is used +in all client redirect URIs and may conflict with existing test suites. diff --git a/FAQ.md b/FAQ.md index 02a955a7..fcf288c5 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,12 +1,12 @@ # Set JSON type for claims You can set the type of claim by prefixing the name with `int:`, `bool:` or `string:`. If no prefix is set then `string` -is assumed. In the rare event that your custom claim name starts with a prefix (example: `int:mycustomclaim`) you can add an one of -the type prefixes (example: `string:int:mycustomclaim`) to force the module to release a claim with the original prefix in it - (example: claim `int:mycustomclaim` of type `string`) +is assumed. In the rare event that your custom claim name starts with a prefix (example: `int:mycustomclaim`) you can +add one of the type prefixes (example: `string:int:mycustomclaim`) to force the module to release a claim with the +original prefix in it (example: claim `int:mycustomclaim` of type `string`) # Release photo -The OIDC `picture` claim is a url, while the `jpegPhoto` ldap attribute is often a b64 string. To use `jpegPhoto` you can -try using an authproc filter to turn it into a data url by adding `data:image/jpeg;base64,` prefix. The support for data urls -amongst OIDC client is unknown. \ No newline at end of file +The OIDC `picture` claim is an URL, while the `jpegPhoto` LDAP attribute is often a b64 string. To use `jpegPhoto` you +can try using an authproc filter to turn it into a data url by adding `data:image/jpeg;base64,` prefix. The support +for data URLs amongst OIDC client is unknown. \ No newline at end of file diff --git a/README.md b/README.md index 1692c5ec..b122b509 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,15 @@ Currently supported flows are: | OIDC module | SimpleSAMLphp | PHP | Note | |:------------|:--------------|:------:|-----------------------------| -| v4.\* | v2.0.\* | \>=8.0 | Recommended | -| v3.\* | v2.0.0 | \>=7.4 | Abandoned from August 2023. | +| v5.\* | v2.1.\* | \>=8.1 | Recommended | +| v4.\* | v2.0.\* | \>=8.0 | | +| v3.\* | v2.0.\* | \>=7.4 | Abandoned from August 2023. | | v2.\* | v1.19.\* | \>=7.4 | | +### Upgrading? + +If you are upgrading from a previous version, checkout the [upgrade guide](UPGRADE.md). + ## Installation Installation can be as easy as executing: @@ -95,10 +100,6 @@ If you use a passphrase, make sure to also configure it in the `module_oidc.php` In order to purge expired tokens, this module requires [cron module](https://simplesamlphp.org/docs/stable/cron:cron) to be enabled and configured. -## Upgrading? - -If you are upgrading from a previous version, checkout the [upgrade guide](UPGRADE.md). - ## Additional considerations ### Private scopes @@ -109,7 +110,7 @@ However, you can add your own private scopes in the `module_oidc.php` config fil [ + \SimpleSAML\Module\oidc\ModuleConfig::OPTION_AUTH_CUSTOM_SCOPES => [ 'private' => [ 'description' => 'private scope', 'claim_name_prefix' => '', // Optional prefix for claim names @@ -131,7 +132,7 @@ You can change or extend this table in the `module_oidc.php` config file, for ex [ + \SimpleSAML\Module\oidc\ModuleConfig::OPTION_AUTH_SAML_TO_OIDC_TRANSLATE_TABLE => [ // Overwrite default translation 'sub' => [ 'uid', // added @@ -185,7 +186,7 @@ documentation](https://simplesamlphp.org/docs/stable/simplesamlphp-authproc). [ + \SimpleSAML\Module\oidc\ModuleConfig::OPTION_AUTH_PROCESSING_FILTERS => [ 50 => [ 'class' => 'core:AttributeAdd', 'groups' => ['users', 'members'], @@ -206,8 +207,8 @@ eduPersonEntitlements from the `client` permission array. A permission can be disabled by commenting it out. -```bash - 'permissions' => [ +```php + \SimpleSAML\Module\oidc\ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS => [ // Attribute to inspect to determine user's permissions 'attribute' => 'eduPersonEntitlement', // Which entitlements allow for registering, editing, delete a client. OIDC clients are owned by the creator @@ -242,9 +243,9 @@ form. Here are some sample configurations: ### With current git branch. -To explore the module using docker run the below command. This will run an SSP image, with the current oidc module mounted -in the container, along with some configuration files. Any code changes you make to your git checkout are "live" in -the container, allowing you to test and iterate different things. +To explore the module using docker run the below command. This will run an SSP image, with the current oidc module +mounted in the container, along with some configuration files. Any code changes you make to your git checkout are +"live" in the container, allowing you to test and iterate different things. ``` GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) diff --git a/UPGRADE.md b/UPGRADE.md index ebc7c234..13b10812 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,7 +1,31 @@ +# Version 4 to 5 + +## Major impact changes +- PHP version requirement was bumped to v8.1 + +## Medium impact changes +- Module config options in file 'module_oidc.php' are now using constants for config keys. The values for constants are +taken from the previous version of the module, so theoretically you don't have to rewrite your current config file, +although it is recommended to do so. + +## Low impact changes +- Removed the 'kid' config option which was not utilized in the codebase (from v2 of the module, the 'kid' value is the +fingerprint of the certificate). + +Below are some internal changes that should not have impact for the OIDC OP implementors. However, if you are using +this module as a library or extending from it, you will probably encounter breaking changes, since a lot of code +has been refactored: + +- psalm error level set to 1, which needed a fair amount of code adjustments +- refactored to strict typing whenever possible (psalm can now infer types for >99% of the codebase) +- refactored to PHP v8.* (up to PHP v8.1) code styling whenever possible, like using constructor property promotion, +match expressions... +- removed dependency on steverhoades/oauth2-openid-connect-server (low maintenance) + # Version 3 to 4 - PHP version requirement was bumped to v8.0 to enable updating important dependant packages like 'league/oauth2-server' which has already moved to PHPv8 between their minor releases. -- SimpleSAMLphp version fixed to v2.0.* +- SimpleSAMLphp version requirement fixed to v2.0.* # Version 2 to 3 - Module code was refactored to make it compatible with SimpleSAMLphp v2 diff --git a/composer.json b/composer.json index 000c2345..14530243 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-curl": "*", "ext-json": "*", "ext-openssl": "*", @@ -28,22 +28,21 @@ "lcobucci/jwt": "^4.1", "league/oauth2-server": "^8.5.3", "nette/forms": "^3", - "psr/container": "^1.0", - "psr/log": "^1.1", - "simplesamlphp/composer-module-installer": "^1.2", + "psr/container": "^2.0", + "psr/log": "^3", + "simplesamlphp/composer-module-installer": "^1.3", "spomky-labs/base64url": "^2.0", - "steverhoades/oauth2-openid-connect-server": "^2.0", - "web-token/jwt-framework": "^2.1" + "symfony/expression-language": "^6.3", + "web-token/jwt-framework": "^3" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3", - "phpunit/php-code-coverage": "^9.0.0", - "phpunit/phpcov": "^8.2.0", - "phpunit/phpunit": "^9.0.0", - "simplesamlphp/simplesamlphp": "2.0.*", - "simplesamlphp/simplesamlphp-test-framework": "^1.2.1", - "squizlabs/php_codesniffer": "^3.7", - "vimeo/psalm": "^5.8" + "phpunit/phpunit": "^10", + "rector/rector": "^0.18.3", + "simplesamlphp/simplesamlphp": "2.1.*", + "simplesamlphp/simplesamlphp-test-framework": "^1.5", + "squizlabs/php_codesniffer": "^3", + "vimeo/psalm": "^5" }, "config": { "preferred-install": { @@ -52,7 +51,8 @@ "sort-packages": true, "allow-plugins": { "simplesamlphp/composer-module-installer": true - } + }, + "cache-dir": "build/composer" }, "autoload": { "psr-4": { @@ -61,12 +61,11 @@ }, "autoload-dev": { "psr-4": { - "SimpleSAML\\Test\\Module\\oidc\\": "tests/" + "SimpleSAML\\Test\\Module\\oidc\\": "tests/src/" } }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" } }, "scripts": { diff --git a/config-templates/module_oidc.php b/config-templates/module_oidc.php index 2452edbe..5b052025 100644 --- a/config-templates/module_oidc.php +++ b/config-templates/module_oidc.php @@ -1,5 +1,7 @@ 20, - - // The private key passphrase (optional) - // 'pass_phrase' => 'secret', - // The cert and key for signing the ID token. Default names are oidc_module.key and oidc_module.crt - // 'privatekey' => 'oidc_module.key', - // 'certificate' => 'oidc_module.crt', - - // Tokens TTL - 'authCodeDuration' => 'PT10M', // 10 minutes - 'refreshTokenDuration' => 'P1M', // 1 month - 'accessTokenDuration' => 'PT1H', // 1 hour, + /** + * PKI (public / private key) related options. + */ + // The private key passphrase (optional). + //ModuleConfig::OPTION_PKI_PRIVATE_KEY_PASSPHRASE => 'secret', + // The certificate and private key filenames for ID token signature handling, with given defaults. + ModuleConfig::OPTION_PKI_PRIVATE_KEY_FILENAME => ModuleConfig::DEFAULT_PKI_PRIVATE_KEY_FILENAME, + ModuleConfig::OPTION_PKI_CERTIFICATE_FILENAME => ModuleConfig::DEFAULT_PKI_CERTIFICATE_FILENAME, - // Tag to run storage cleanup script using the cron module... - 'cron_tag' => 'hourly', + /** + * Token related options. + */ + // Authorization code and tokens TTL (validity duration), with given examples. For duration format info, check + // https://www.php.net/manual/en/dateinterval.construct.php + ModuleConfig::OPTION_TOKEN_AUTHORIZATION_CODE_TTL => 'PT10M', // 10 minutes + ModuleConfig::OPTION_TOKEN_REFRESH_TOKEN_TTL => 'P1M', // 1 month + ModuleConfig::OPTION_TOKEN_ACCESS_TOKEN_TTL => 'PT1H', // 1 hour, - // Set token signer + // Token signer, with given default. // See Lcobucci\JWT\Signer algorithms in https://github.com/lcobucci/jwt/tree/master/src/Signer - 'signer' => \Lcobucci\JWT\Signer\Rsa\Sha256::class, - // 'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, - // 'signer' => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class, - - // The key id to use in the header. Default is a finger print of the public key - // 'kid' => 'abcd', - - - // this is the default auth source used for authentication if the auth source - // is not specified on particular client - 'auth' => 'default-sp', - - // useridattr is the attribute-name that contains the userid as returned from idp. By default, this attribute - // will be dynamically added to the 'sub' claim in the attribute-to-claim translation table (you will probably - // want to use this attribute as the 'sub' claim since it designates unique identifier for the user). - 'useridattr' => 'uid', + ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Rsa\Sha256::class, + //ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Hmac\Sha256::class, + //ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class, /** - * Permissions let the module expose functionality to specific users. - * In the below configuration, a user's eduPersonEntitlement attribute is examined. If the user - * tries to do something that requires the 'client' permission (such as registering their own client) - * then they will need one of the eduPersonEntitlements from the `client` permission array. - * - * A permission can be disable by commenting it out. + * Authentication related options. */ - 'permissions' => [ - // Attribute to inspect to determine user's permissions - 'attribute' => 'eduPersonEntitlement', - // Which entitlements allow for registering, editing, delete a client. OIDC clients are owned by the creator - 'client' => ['urn:example:oidc:manage:client'], - ], + // The default authentication source to be used for authentication if the auth source is not specified on + // particular client. + ModuleConfig::OPTION_AUTH_SOURCE => 'default-sp', - // Settings regarding Authentication Processing Filters. - // Note: OIDC authN state array will not contain all of the keys which are available during SAML authN, - // like Service Provider metadata, etc. - // - // At the moment, the following SAML authN data will be available during OIDC authN in the sate array: - // - ['Attributes'], ['Authority'], ['AuthnInstant'], ['Expire'] - // Source and destination will have entity IDs corresponding to the OP issuer ID and Client ID respectively. - // - ['Source']['entityid'] - contains OpenId Provider issuer ID - // - ['Destination']['entityid'] - contains Relying Party (OIDC Client) ID - // In addition to that, the following OIDC related data will be available in the state array: - // - ['Oidc']['OpenIdProviderMetadata'] - contains information otherwise available from the OIDC configuration URL. - // - ['Oidc']['RelyingPartyMetadata'] - contains information about the OIDC client making the authN request. - // - ['Oidc']['AuthorizationRequestParameters'] - contains relevant authorization request query parameters. - // - // List of authproc filters which will run for every OIDC authN. Add filters as described in docs for SAML authproc - // @see https://simplesamlphp.org/docs/stable/simplesamlphp-authproc - 'authproc.oidc' => [ - // Add authproc filters here - ], + // The attribute name that contains the user identifier returned from IdP. By default, this attribute will be + // dynamically added to the 'sub' claim in the attribute-to-claim translation table (you will probably want + // to use this attribute as the 'sub' claim since it designates unique identifier for the user). + ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE => 'uid', - // Optional custom scopes. You can create as many scopes as you want and assign claims to them. - 'scopes' => [ -// 'private' => [ // The key represents the scope name. -// 'description' => 'private scope', -// 'claim_name_prefix' => '', // Prefix to apply for all claim names from this scope -// 'are_multiple_claim_values_allowed' => false, // Are claims for this scope allowed to have multiple values -// 'claims' => ['national_document_id'] // Claims from the translation table which this scope will contain -// ], - ], - 'translate' => [ + // The default translate table from SAML attributes to OIDC claims. + ModuleConfig::OPTION_AUTH_SAML_TO_OIDC_TRANSLATE_TABLE => [ /* - * This is the default translate table from SAML to OIDC. - * You can change here the behaviour or add more translation to your - * private attributes scopes - * * The basic format is * * 'claimName' => [ @@ -149,7 +109,7 @@ // 'description', // ], // 'picture' => [ -// // Empty. Previously 'jpegPhoto' however spec calls for a url to photo, not an actual photo. +// // Empty. Previously 'jpegPhoto' however spec calls for a URL to photo, not an actual photo. // ], // 'website' => [ // // Empty @@ -177,7 +137,7 @@ // 'type' => 'bool', // 'attributes' => [], // ], -// // address is a json object. Set the 'formatted' sub claim to postalAddress +// // address is a json object. Set the 'formatted' sub-claim to postalAddress // 'address' => [ // 'type' => 'json', // 'claims' => [ @@ -201,6 +161,16 @@ // ], ], + // Optional custom scopes. You can create as many scopes as you want and assign claims to them. + ModuleConfig::OPTION_AUTH_CUSTOM_SCOPES => [ +// 'private' => [ // The key represents the scope name. +// 'description' => 'private scope', +// 'claim_name_prefix' => '', // Prefix to apply for all claim names from this scope +// 'are_multiple_claim_values_allowed' => false, // Are claims for this scope allowed to have multiple values +// 'claims' => ['national_document_id'] // Claims from the translation table which this scope will contain +// ], + ], + // Optional list of the Authentication Context Class References that this OP supports. // If populated, this list will be available in OP discovery document (OP Metadata) as 'acr_values_supported'. // @see https://datatracker.ietf.org/doc/html/rfc6711 @@ -208,7 +178,7 @@ // @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken (acr claim) // @see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest (acr_values parameter) // Syntax: string[] (array of strings) - 'acrValuesSupported' => [ + ModuleConfig::OPTION_AUTH_ACR_VALUES_SUPPORTED => [ // 'https://refeds.org/assurance/profile/espresso', // 'https://refeds.org/assurance/profile/cappuccino', // 'https://refeds.org/profile/mfa', @@ -226,7 +196,7 @@ // If this OP supports ACRs, indicate which usable auth source supports which ACRs. // Order of ACRs is important, more important ones being first. // Syntax: array (array with auth source as key and value being array of ACR values as strings) - 'authSourcesToAcrValuesMap' => [ + ModuleConfig::OPTION_AUTH_SOURCES_TO_ACR_VALUES_MAP => [ // 'example-userpass' => ['1', '0'], // 'default-sp' => ['http://id.incommon.org/assurance/bronze', '2', '1', '0'], // 'strongly-assured-authsource' => [ @@ -248,6 +218,49 @@ // set to that ACR for cookie authentication. // For example, OIDC Core Spec notes that authentication using a long-lived browser cookie is one example where // the use of "level 0" is appropriate: -// 'forcedAcrValueForCookieAuthentication' => '0', - 'forcedAcrValueForCookieAuthentication' => null, +// ModuleConfig::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION => '0', + ModuleConfig::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION => null, + + // Settings regarding Authentication Processing Filters. + // Note: OIDC authN state array will not contain all the keys which are available during SAML authN, + // like Service Provider metadata, etc. + // + // At the moment, the following SAML authN data will be available during OIDC authN in the sate array: + // - ['Attributes'], ['Authority'], ['AuthnInstant'], ['Expire'] + // Source and destination will have entity IDs corresponding to the OP issuer ID and Client ID respectively. + // - ['Source']['entityid'] - contains OpenId Provider issuer ID + // - ['Destination']['entityid'] - contains Relying Party (OIDC Client) ID + // In addition to that, the following OIDC related data will be available in the state array: + // - ['Oidc']['OpenIdProviderMetadata'] - contains information otherwise available from the OIDC configuration URL. + // - ['Oidc']['RelyingPartyMetadata'] - contains information about the OIDC client making the authN request. + // - ['Oidc']['AuthorizationRequestParameters'] - contains relevant authorization request query parameters. + // + // List of authproc filters which will run for every OIDC authN. Add filters as described in docs for SAML authproc + // @see https://simplesamlphp.org/docs/stable/simplesamlphp-authproc + ModuleConfig::OPTION_AUTH_PROCESSING_FILTERS => [ + // Add authproc filters here + ], + + /** + * Cron related options. + */ + // Cron tag used to run storage cleanup script using the cron module. + ModuleConfig::OPTION_CRON_TAG => 'hourly', + + /** + * Admin backend UI related options. + */ + // Permissions which let the module expose functionality to specific users. In the below configuration, a user's + // eduPersonEntitlement attribute is examined. If the user tries to do something that requires the 'client' + // permission (such as registering their own client), then they will need one of the eduPersonEntitlements + // from the `client` permission array. A permission can be disabled by commenting it out. + ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS => [ + // Attribute to inspect to determine user's permissions + 'attribute' => 'eduPersonEntitlement', + // Which entitlements allow for registering, editing, delete a client. OIDC clients are owned by the creator + 'client' => ['urn:example:oidc:manage:client'], + ], + + // Pagination options. + ModuleConfig::OPTION_ADMIN_UI_PAGINATION_ITEMS_PER_PAGE => 20, ]; diff --git a/docker/Dockerfile b/docker/Dockerfile index d33aa180..18946657 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM cirrusid/simplesamlphp:v2.0.0 +#FROM cirrusid/simplesamlphp:v2.0.0 +FROM cicnavi/simplesamlphp:dev-simplesamlphp-2.1 RUN apt-get update && apt-get install -y sqlite3 # Prepopulate the DB with items needed for testing diff --git a/docker/nginx-certs/README.md b/docker/nginx-certs/README.md index 17e67af3..31efd918 100644 --- a/docker/nginx-certs/README.md +++ b/docker/nginx-certs/README.md @@ -1,5 +1,4 @@ -Every 90 days these certificates expire. -The upstream project/container will refresh its certs occassionaly and we +Every 90 days these certificates expire. The upstream project/container will refresh its certs occasionally, and we can sync them here. ```bash diff --git a/docker/ssp/authsources.php b/docker/ssp/authsources.php index 68e29348..7e84dc87 100644 --- a/docker/ssp/authsources.php +++ b/docker/ssp/authsources.php @@ -1,8 +1,10 @@ array( 'core:AdminPassword', ), diff --git a/docker/ssp/config-override.php b/docker/ssp/config-override.php index d7dae4ea..c0e92ac2 100644 --- a/docker/ssp/config-override.php +++ b/docker/ssp/config-override.php @@ -1,4 +1,7 @@ 'secret', - - // Tokens TTL - 'authCodeDuration' => 'PT10M', // 10 minutes - 'refreshTokenDuration' => 'P1M', // 1 month - 'accessTokenDuration' => 'PT1H', // 1 hour, +use SimpleSAML\Module\oidc\ModuleConfig; - // Tag to run storage cleanup script using the cron module... - 'cron_tag' => 'hourly', +$config = [ + ModuleConfig::OPTION_TOKEN_AUTHORIZATION_CODE_TTL => 'PT10M', + ModuleConfig::OPTION_TOKEN_REFRESH_TOKEN_TTL => 'P1M', + ModuleConfig::OPTION_TOKEN_ACCESS_TOKEN_TTL => 'PT1H', - // Set token signer - // See Lcobucci\JWT\Signer algorithms in https://github.com/lcobucci/jwt/tree/master/src/Signer - 'signer' => \Lcobucci\JWT\Signer\Rsa\Sha256::class, - // 'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, - // 'signer' => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class, + ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Rsa\Sha256::class, - // this is the default auth source used for authentication if the auth source - // is not specified on particular client - 'auth' => 'example-userpass', + ModuleConfig::OPTION_AUTH_SOURCE => 'example-userpass', - // useridattr is the attribute-name that contains the userid as returned from idp. By default, this attribute - // will be dynamically added to the 'sub' claim in the attribute-to-claim translation table (you will probably - // want to use this attribute as the 'sub' claim since it designates unique identifier for the user). - 'useridattr' => 'uid', - 'permissions' => [ - // Attribute to inspect to determine user's permissions + ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE => 'uid', + ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS => [ 'attribute' => 'eduPersonEntitlement', - // Which entitlements allow for registering, editing, delete a client. OIDC clients are owned by the creator 'client' => ['urn:example:oidc:manage:client'], ], - // Settings regarding Authentication Processing Filters. - // Note: OIDC authN state array will not contain all of the keys which are available during SAML authN, - // like Service Provider metadata, etc. - // - // At the moment, the following SAML authN data will be available during OIDC authN in the sate array: - // - 'Attributes', 'Authority', 'AuthnInstant', 'Expire', 'IdPMetadata', 'Source' - // In addition to that, the following OIDC related data will be available in the state array: - // - 'OidcProviderMetadata' - contains information otherwise available from the OIDC configuration URL. - // - 'OidcRelyingPartyMetadata' - contains information about the OIDC client making the authN request. - // - 'OidcAuthorizationRequestParameters' - contains relevant authorization request query parameters. - // - // List of authproc filters which will run for every OIDC authN. Add filters as described in docs for SAML authproc - // @see https://simplesamlphp.org/docs/stable/simplesamlphp-authproc - 'authproc.oidc' => [ - // Add authproc filters here + ModuleConfig::OPTION_AUTH_PROCESSING_FILTERS => [ ], - // Optional custom scopes. You can create as many scopes as you want and assign claims to them. - 'scopes' => [ -// 'private' => [ // The key represents the scope name. -// 'description' => 'private scope', -// 'claim_name_prefix' => '', // Prefix to apply for all claim names from this scope -// 'are_multiple_claim_values_allowed' => false, // Are claims for this scope allowed to have multiple values -// 'claims' => ['national_document_id'] // Claims from the translation table which this scope will contain -// ], + ModuleConfig::OPTION_AUTH_CUSTOM_SCOPES => [ ], - 'translate' => [ - /* - * This is the default translate table from SAML to OIDC. - * You can change here the behaviour or add more translation to your - * private attributes scopes - * - * Note on 'sub' claim: by default, the list of attributes for 'sub' claim will also contain attribute defined - * in 'useridattr' setting. You will probably want to use this attribute as the 'sub' claim since it - * designates unique identifier for the user, However, override as necessary. - */ -// 'sub' => [ -// 'attribute-defined-in-useridattr', // will be dynamically added if the list for 'sub' claim is not set. -// 'eduPersonPrincipalName', -// 'eduPersonTargetedID', -// 'eduPersonUniqueId', -// ], -// 'name' => [ -// 'cn', -// 'displayName', -// ], -// 'family_name' => [ -// 'sn', -// ], -// 'given_name' => [ -// 'givenName', -// ], + ModuleConfig::OPTION_AUTH_SAML_TO_OIDC_TRANSLATE_TABLE => [ 'middle_name' => [ 'middle_name' ], -// 'nickname' => [ -// 'eduPersonNickname', -// ], -// 'preferred_username' => [ -// 'uid', -// ], -// 'profile' => [ -// 'labeledURI', -// 'description', -// ], 'picture' => [ 'jpegURL', ], @@ -125,16 +55,10 @@ 'zoneinfo' => [ 'zoneinfo' ], -// 'locale' => [ -// 'preferredLanguage', -// ], 'updated_at' => [ 'type' => 'int', 'updated_at' ], -// 'email' => [ -// 'mail', -// ], 'email_verified' => [ 'type' => 'bool', 'attributes' => ['email_verified'] @@ -150,50 +74,22 @@ 'country' => ['country'], ] ], - -// 'phone_number' => [ -// 'mobile', -// 'telephoneNumber', -// 'homePhone', -// ], 'phone_number_verified' => [ 'type' => 'bool', 'phone_number_verified' ], - /* - * Optional scopes attributes - */ -// 'national_document_id' => [ -// 'schacPersonalUniqueId', -// ], ], - // Optional list of the Authentication Context Class References that this OP supports. - // If populated, this list will be available in OP discovery document (OP Metadata) as 'acr_values_supported'. - // @see https://datatracker.ietf.org/doc/html/rfc6711 - // @see https://www.iana.org/assignments/loa-profiles/loa-profiles.xhtml - // @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken (acr claim) - // @see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest (acr_values parameter) - // Syntax: string[] (array of strings) - 'acrValuesSupported' => [ + ModuleConfig::OPTION_AUTH_ACR_VALUES_SUPPORTED => [ '1', '0', ], - // If this OP supports ACRs, indicate which usable auth source supports which ACRs. - // Order of ACRs is important, more important ones being first. - // Syntax: array (array with auth source as key and value being array of ACR values as strings) - 'authSourcesToAcrValuesMap' => [ + ModuleConfig::OPTION_AUTH_SOURCES_TO_ACR_VALUES_MAP => [ 'example-userpass' => ['1', '0'], ], - // If this OP supports ACRs, indicate if authentication using cookie should be forced to specific ACR value. - // If this option is set to null, no specific ACR will be forced for cookie authentication and the resulting ACR - // will be one of the ACRs supported on used auth source during authentication, that is, session creation. - // If this option is set to specific ACR, with ACR value being one of the ACR value this OP supports, it will be - // set to that ACR for cookie authentication. - // For example, OIDC Core Spec notes that authentication using a long-lived browser cookie is one example where - // the use of "level 0" is appropriate: -// 'forcedAcrValueForCookieAuthentication' => '0', - 'forcedAcrValueForCookieAuthentication' => null, + ModuleConfig::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION => null, + + ModuleConfig::OPTION_CRON_TAG => 'hourly', ]; diff --git a/hooks/hook_cron.php b/hooks/hook_cron.php index 35690d54..1ea969ae 100644 --- a/hooks/hook_cron.php +++ b/hooks/hook_cron.php @@ -1,5 +1,7 @@ config(); - if (null === $oidcConfig->getOptionalValue('cron_tag', null)) { + if (null === $oidcConfig->getOptionalValue(ModuleConfig::OPTION_CRON_TAG, null)) { return; } - if ($oidcConfig->getOptionalValue('cron_tag', null) !== $croninfo['tag']) { + if ($oidcConfig->getOptionalValue(ModuleConfig::OPTION_CRON_TAG, null) !== $croninfo['tag']) { return; } - $container = new \SimpleSAML\Module\oidc\Services\Container(); + $container = new Container(); try { + /** @var AccessTokenRepository $accessTokenRepository */ $accessTokenRepository = $container->get(AccessTokenRepository::class); $accessTokenRepository->removeExpired(); + /** @var AuthCodeRepository $authTokenRepository */ $authTokenRepository = $container->get(AuthCodeRepository::class); $authTokenRepository->removeExpired(); + /** @var RefreshTokenRepository $refreshTokenRepository */ $refreshTokenRepository = $container->get(RefreshTokenRepository::class); $refreshTokenRepository->removeExpired(); $croninfo['summary'][] = 'Module `oidc` clean up. Removed expired entries from storage.'; } catch (Exception $e) { $message = 'Module `oidc` clean up cron script failed: ' . $e->getMessage(); - \SimpleSAML\Logger::warning($message); + Logger::warning($message); $croninfo['summary'][] = $message; } } diff --git a/hooks/hook_federationpage.php b/hooks/hook_federationpage.php index b5abda50..dca1cca7 100644 --- a/hooks/hook_federationpage.php +++ b/hooks/hook_federationpage.php @@ -1,5 +1,7 @@ isUpdated()) { - $href = \SimpleSAML\Module::getModuleURL('oidc/install.php'); + if (! (new DatabaseMigration())->isUpdated()) { + $href = Module::getModuleURL('oidc/install.php'); $text = Translate::noop('OpenID Connect Installation'); } + if (!is_array($template->data['links'])) { + $template->data['links'] = []; + } + $template->data['links'][] = [ 'href' => $href, 'text' => $text, diff --git a/hooks/hook_frontpage.php b/hooks/hook_frontpage.php index 5a7f708e..7b642389 100644 --- a/hooks/hook_frontpage.php +++ b/hooks/hook_frontpage.php @@ -1,5 +1,7 @@ isUpdated(); + $isUpdated = (new DatabaseMigration())->isUpdated(); if (!$isUpdated) { $links['federation']['oidcregistry'] = [ - 'href' => \SimpleSAML\Module::getModuleURL('oidc/install.php'), + 'href' => Module::getModuleURL('oidc/install.php'), 'text' => [ 'en' => 'OpenID Connect Installation', 'es' => 'InstalaciĆ³n de OpenID Connect', @@ -43,7 +44,7 @@ function oidc_hook_frontpage(&$links) } $links['federation']['oidcregistry'] = [ - 'href' => \SimpleSAML\Module::getModuleURL('oidc/admin-clients/index.php'), + 'href' => Module::getModuleURL('oidc/admin-clients/index.php'), 'text' => [ 'en' => 'OpenID Connect Client Registry', 'es' => 'Registro de clientes OpenID Connect', diff --git a/phpcs.xml b/phpcs.xml index bbf97ec2..edcdc292 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -6,6 +6,9 @@ By default it is less stringent about long lines than other coding standards + + + config-templates hooks src @@ -17,12 +20,14 @@ public/assets/* - - - spec/* - - + + + + + + + diff --git a/phpunit.xml b/phpunit.xml index 5390b395..ac79ebb1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,19 +1,11 @@ - - - ./src - + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" + cacheDirectory="./build/phpunit-cache" +> + @@ -30,4 +22,9 @@ + + + ./src + + diff --git a/psalm.xml b/psalm.xml index d1867929..ddac7608 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,61 +1,49 @@ - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/public/admin-clients/delete.php b/public/admin-clients/delete.php index 9ef89c03..a1920f98 100644 --- a/public/admin-clients/delete.php +++ b/public/admin-clients/delete.php @@ -1,5 +1,7 @@ importNames(); + $rectorConfig->disableParallel(); + + $rectorConfig->bootstrapFiles([ + //__DIR__ . '/vendor/autoload.php', + ]); + + $rectorConfig->paths([ + // TODO mivanci also go trough commented out paths... + //__DIR__ . '/docker', + //__DIR__ . '/hooks', + //__DIR__ . '/public', + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + // register a single rule + $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); + $rectorConfig->rule(DeclareStrictTypesRector::class); + + // define sets of rules + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_81, + ]); +}; diff --git a/routing/routes/routes.yml b/routing/routes/routes.yml index 1b6661af..0abd5cb5 100644 --- a/routing/routes/routes.yml +++ b/routing/routes/routes.yml @@ -1,6 +1,9 @@ -# TODO sspv2 move to routes in SSP v2 #oidc-clients: # path: /clients # controller: SimpleSAML\Module\oidc\Services\RoutingService::call # defaults: -# controllerClassname: SimpleSAML\Module\oidc\Controller\ClientIndexController +# controllerClassname: SimpleSAML\Module\oidc\Controller\Client\IndexController + +openid-configuration: + path: openid-configuration + controller: SimpleSAML\Module\oidc\Controller\ConfigurationDiscoveryController \ No newline at end of file diff --git a/routing/services/services.yml b/routing/services/services.yml index ee3f0201..c9daf55d 100644 --- a/routing/services/services.yml +++ b/routing/services/services.yml @@ -1,27 +1,54 @@ services: - # default configuration for services in *this* file - _defaults: - public: false + # default configuration for services in *this* file + _defaults: + autowire: true + autoconfigure: true + public: false + bind: + #Psr\Log\LoggerInterface: '@SimpleSAML\Module\oidc\Services\LoggerService' + League\OAuth2\Server\Repositories\ClientRepositoryInterface: '@SimpleSAML\Module\oidc\Repositories\ClientRepository' + League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface: '@SimpleSAML\Module\oidc\Repositories\AccessTokenRepository' + League\OAuth2\Server\Repositories\ScopeRepositoryInterface: '@SimpleSAML\Module\oidc\Repositories\ScopeRepository' + League\OAuth2\Server\CryptKey|string $privateKey: '@oidc.key.private' + League\OAuth2\Server\CryptKey|string $publicKey: '@oidc.key.public' - SimpleSAML\Module\oidc\Services\: - resource: '../../src/Services/*' - exclude: '../../src/Services/{Container.php}' + SimpleSAML\Module\oidc\Services\: + resource: '../../src/Services/*' + exclude: '../../src/Services/{Container.php}' - SimpleSAML\Module\oidc\Repositories\: - resource: '../../src/Repositories/*' - exclude: '../../src/Repositories/{Interfaces}' + SimpleSAML\Module\oidc\Repositories\: + resource: '../../src/Repositories/*' + exclude: '../../src/Repositories/{Interfaces}' - SimpleSAML\Module\oidc\Factories\: - resource: '../../src/Factories/*' + SimpleSAML\Module\oidc\Factories\: + resource: '../../src/Factories/*' - SimpleSAML\Module\oidc\Store\: - resource: '../../src/Store/*' + SimpleSAML\Module\oidc\Stores\: + resource: '../../src/Stores/*' - SimpleSAML\Module\oidc\Server\AuthorizationServer: - class: SimpleSAML\Module\oidc\Server\AuthorizationServer + SimpleSAML\Module\oidc\ModuleConfig: ~ - League\OAuth2\Server\ResourceServer: - class: League\OAuth2\Server\ResourceServer + oidc.key.private: + class: League\OAuth2\Server\CryptKey + factory: ['@SimpleSAML\Module\oidc\Factories\CryptKeyFactory', 'buildPrivateKey'] - SimpleSAML\Module\oidc\ClaimTranslatorExtractor: - class: SimpleSAML\Module\oidc\ClaimTranslatorExtractor + oidc.key.public: + class: League\OAuth2\Server\CryptKey + factory: ['@SimpleSAML\Module\oidc\Factories\CryptKeyFactory', 'buildPublicKey'] + + SimpleSAML\Module\oidc\Factories\ResourceServerFactory: + arguments: + $publicKey: '@oidc.key.public' + + SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor: + arguments: + $userIdAttr: '@=service("SimpleSAML\\Module\\oidc\\ModuleConfig").getUserIdentifierAttribute()' + + SimpleSAML\Module\oidc\Server\AuthorizationServer: + arguments: + $encryptionKey: '@=service("SimpleSAML\\Module\\oidc\\ModuleConfig").getEncryptionKey()' + + # OAuth2 Server + League\OAuth2\Server\ResourceServer: + arguments: + $publicKey: '@oidc.key.public' diff --git a/src/Controller/OAuth2AccessTokenController.php b/src/Controller/AccessTokenController.php similarity index 67% rename from src/Controller/OAuth2AccessTokenController.php rename to src/Controller/AccessTokenController.php index 89432558..3988e80b 100644 --- a/src/Controller/OAuth2AccessTokenController.php +++ b/src/Controller/AccessTokenController.php @@ -1,5 +1,7 @@ authorizationServer = $authorizationServer; } - public function __invoke(ServerRequest $request): \Psr\Http\Message\ResponseInterface + /** + * @throws OAuthServerException + */ + public function __invoke(ServerRequest $request): ResponseInterface { return $this->authorizationServer->respondToAccessTokenRequest($request, new Response()); } diff --git a/src/Controller/OAuth2AuthorizationController.php b/src/Controller/AuthorizationController.php similarity index 72% rename from src/Controller/OAuth2AuthorizationController.php rename to src/Controller/AuthorizationController.php index beb69424..33799b56 100644 --- a/src/Controller/OAuth2AuthorizationController.php +++ b/src/Controller/AuthorizationController.php @@ -1,5 +1,7 @@ authenticationService = $authenticationService; - $this->authorizationServer = $authorizationServer; - $this->configurationService = $configurationService; - $this->loggerService = $loggerService; } /** @@ -61,7 +46,7 @@ public function __construct( * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException - * @throws Exception + * @throws Exception|Throwable */ public function __invoke(ServerRequest $request): ResponseInterface { @@ -86,19 +71,17 @@ public function __invoke(ServerRequest $request): ResponseInterface /** * Validate authorization request after the authn has been performed. For example, check if the * ACR claim has been requested and that authn performed satisfies it. - * @param AuthorizationRequest $authorizationRequest * @throws Exception */ - protected function validatePostAuthnAuthorizationRequest(AuthorizationRequest &$authorizationRequest) + protected function validatePostAuthnAuthorizationRequest(AuthorizationRequest $authorizationRequest): void { $this->validateAcr($authorizationRequest); } /** - * @param AuthorizationRequest $authorizationRequest * @throws Exception */ - protected function validateAcr(AuthorizationRequest &$authorizationRequest): void + protected function validateAcr(AuthorizationRequest $authorizationRequest): void { // If no ACRs requested, don't set ACR claim. if (($requestedAcrValues = $authorizationRequest->getRequestedAcrValues()) === null) { @@ -113,20 +96,30 @@ protected function validateAcr(AuthorizationRequest &$authorizationRequest): voi throw OidcServerException::serverError('isCookieBasedAuthn not set on authz request'); } - $availableAuthSourceAcrs = $this->configurationService->getAuthSourcesToAcrValuesMap()[$authSourceId] ?? []; - $forcedAcrForCookieAuthentication = $this->configurationService->getForcedAcrValueForCookieAuthentication(); + $authSourceToAcrValuesMap = $this->moduleConfig->getAuthSourcesToAcrValuesMap(); + + $availableAuthSourceAcrs = is_array($authSourceToAcrValuesMap[$authSourceId]) ? + $authSourceToAcrValuesMap[$authSourceId] : + []; + $forcedAcrForCookieAuthentication = $this->moduleConfig->getForcedAcrValueForCookieAuthentication(); if ($forcedAcrForCookieAuthentication !== null && $isCookieBasedAuthn) { $availableAuthSourceAcrs = [$forcedAcrForCookieAuthentication]; } - $isRequestedAcrEssential = $requestedAcrValues['essential'] ?? false; + $isRequestedAcrEssential = empty($requestedAcrValues['essential']) ? + false : + boolval($requestedAcrValues['essential']); + + $acrs = !empty($requestedAcrValues['values']) && is_array($requestedAcrValues['values']) ? + $requestedAcrValues['values'] : + []; - $matchedAcrs = array_intersect($availableAuthSourceAcrs, $requestedAcrValues['values']); + $matchedAcrs = array_intersect($availableAuthSourceAcrs, $acrs); // If we have matched ACRs, use the best (first) one (order is important). if (!empty($matchedAcrs)) { - $authorizationRequest->setAcr(current($matchedAcrs)); + $authorizationRequest->setAcr((string)current($matchedAcrs)); return; } @@ -137,7 +130,7 @@ protected function validateAcr(AuthorizationRequest &$authorizationRequest): voi // If the ACR is not essential, we should return current session ACR (if we have one available)... if (! empty($availableAuthSourceAcrs)) { - $authorizationRequest->setAcr(current($availableAuthSourceAcrs)); + $authorizationRequest->setAcr((string)current($availableAuthSourceAcrs)); return; } diff --git a/src/Controller/ClientCreateController.php b/src/Controller/Client/CreateController.php similarity index 53% rename from src/Controller/ClientCreateController.php rename to src/Controller/Client/CreateController.php index 41119051..126ada66 100644 --- a/src/Controller/ClientCreateController.php +++ b/src/Controller/Client/CreateController.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; - $this->allowedOriginRepository = $allowedOriginRepository; - $this->templateFactory = $templateFactory; - $this->formFactory = $formFactory; - $this->messages = $messages; - $this->authContextService = $authContextService; } /** * @return RedirectResponse|Template * @throws Exception + * @throws \Exception */ - public function __invoke(ServerRequest $request) + public function __invoke(): Template|RedirectResponse { /** @var ClientForm $form */ $form = $this->formFactory->build(ClientForm::class); @@ -78,23 +63,43 @@ public function __invoke(ServerRequest $request) $client['owner'] = $this->authContextService->getAuthUserId(); } + if ( + !is_string($client['name']) || + !is_string($client['description']) || + !is_array($client['redirect_uri']) || + !is_array($client['scopes']) || + !is_array($client['post_logout_redirect_uri']) || + !is_array($client['allowed_origin']) + ) { + throw OidcServerException::serverError('Invalid Client Entity data'); + } + + /** @var string[] $redirectUris */ + $redirectUris = $client['redirect_uri']; + /** @var string[] $scopes */ + $scopes = $client['scopes']; + /** @var string[] $postLogoutRedirectUris */ + $postLogoutRedirectUris = $client['post_logout_redirect_uri']; + /** @var string[] $allowedOrigins */ + $allowedOrigins = $client['allowed_origin']; + $this->clientRepository->add(ClientEntity::fromData( $client['id'], $client['secret'], $client['name'], $client['description'], - $client['redirect_uri'], - $client['scopes'], - $client['is_enabled'], - $client['is_confidential'], - $client['auth_source'], - $client['owner'] ?? null, - $client['post_logout_redirect_uri'], - $client['backchannel_logout_uri'] + $redirectUris, + $scopes, + (bool)$client['is_enabled'], + (bool)$client['is_confidential'], + empty($client['auth_source']) ? null : (string)$client['auth_source'], + empty($client['owner']) ? null : (string)$client['owner'], + $postLogoutRedirectUris, + empty($client['backchannel_logout_uri']) ? null : (string)$client['backchannel_logout_uri'] )); // Also persist allowed origins for this client. - $this->allowedOriginRepository->set($client['id'], $client['allowed_origin']); + $this->allowedOriginRepository->set($client['id'], $allowedOrigins); $this->messages->addMessage('{oidc:client:added}'); diff --git a/src/Controller/ClientDeleteController.php b/src/Controller/Client/DeleteController.php similarity index 74% rename from src/Controller/ClientDeleteController.php rename to src/Controller/Client/DeleteController.php index 73fa1b4b..c48112fd 100644 --- a/src/Controller/ClientDeleteController.php +++ b/src/Controller/Client/DeleteController.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; - $this->templateFactory = $templateFactory; - $this->messages = $messages; $this->authContextService = $authContextService; } /** - * @return \Laminas\Diactoros\Response\RedirectResponse|\SimpleSAML\XHTML\Template + * @throws ConfigurationError|BadRequest|NotFound|Exception|OidcServerException|JsonException + * @throws \Exception */ - public function __invoke(ServerRequest $request) + public function __invoke(ServerRequest $request): Template|RedirectResponse { $client = $this->getClientFromRequest($request); $body = $request->getParsedBody(); - $clientSecret = $body['secret'] ?? null; + $clientSecret = empty($body['secret']) ? null : (string)$body['secret']; $authedUser = $this->authContextService->isSspAdmin() ? null : $this->authContextService->getAuthUserId(); if ('POST' === mb_strtoupper($request->getMethod())) { if (!$clientSecret) { diff --git a/src/Controller/ClientEditController.php b/src/Controller/Client/EditController.php similarity index 62% rename from src/Controller/ClientEditController.php rename to src/Controller/Client/EditController.php index 47800daf..2e06240d 100644 --- a/src/Controller/ClientEditController.php +++ b/src/Controller/Client/EditController.php @@ -1,5 +1,7 @@ configurationService = $configurationService; $this->clientRepository = $clientRepository; - $this->allowedOriginRepository = $allowedOriginRepository; - $this->templateFactory = $templateFactory; - $this->formFactory = $formFactory; - $this->messages = $messages; $this->authContextService = $authContextService; } /** - * @return RedirectResponse|Template - * @throws Exception + * @throws BadRequest|Exception|NotFound|\Exception */ - public function __invoke(ServerRequest $request) + public function __invoke(ServerRequest $request): Template|RedirectResponse { - $client = $this->getClientFromRequest($request); $clientAllowedOrigins = $this->allowedOriginRepository->get($client->getIdentifier()); @@ -76,7 +62,6 @@ public function __invoke(ServerRequest $request) $form = $this->formFactory->build(ClientForm::class); $formAction = $request->withQueryParams(['client_id' => $client->getIdentifier()])->getRequestTarget(); $form->setAction($formAction); - $clientData = $client->toArray(); $clientData['allowed_origin'] = $clientAllowedOrigins; $form->setDefaults($clientData); @@ -85,23 +70,43 @@ public function __invoke(ServerRequest $request) if ($form->isSuccess()) { $data = $form->getValues(); + if ( + !is_string($data['name']) || + !is_string($data['description']) || + !is_array($data['redirect_uri']) || + !is_array($data['scopes']) || + !is_array($data['post_logout_redirect_uri']) || + !is_array($data['allowed_origin']) + ) { + throw OidcServerException::serverError('Invalid Client Entity data'); + } + + /** @var string[] $redirectUris */ + $redirectUris = $data['redirect_uri']; + /** @var string[] $scopes */ + $scopes = $data['scopes']; + /** @var string[] $postLogoutRedirectUris */ + $postLogoutRedirectUris = $data['post_logout_redirect_uri']; + /** @var string[] $allowedOrigins */ + $allowedOrigins = $data['allowed_origin']; + $this->clientRepository->update(ClientEntity::fromData( $client->getIdentifier(), $client->getSecret(), $data['name'], $data['description'], - $data['redirect_uri'], - $data['scopes'], + $redirectUris, + $scopes, (bool) $data['is_enabled'], (bool) $data['is_confidential'], - $data['auth_source'], + empty($data['auth_source']) ? null : (string)$data['auth_source'], $client->getOwner(), - $data['post_logout_redirect_uri'], - $data['backchannel_logout_uri'] + $postLogoutRedirectUris, + empty($data['backchannel_logout_uri']) ? null : (string)$data['backchannel_logout_uri'] ), $authedUser); // Also persist allowed origins for this client. - $this->allowedOriginRepository->set($client->getIdentifier(), $data['allowed_origin']); + $this->allowedOriginRepository->set($client->getIdentifier(), $allowedOrigins); $this->messages->addMessage('{oidc:client:updated}'); diff --git a/src/Controller/ClientIndexController.php b/src/Controller/Client/IndexController.php similarity index 68% rename from src/Controller/ClientIndexController.php rename to src/Controller/Client/IndexController.php index 6f64405a..84f5b8f2 100644 --- a/src/Controller/ClientIndexController.php +++ b/src/Controller/Client/IndexController.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; - $this->templateFactory = $templateFactory; - $this->authContextService = $authContextService; } - public function __invoke(ServerRequest $request): \SimpleSAML\XHTML\Template + /** + * @throws Exception + * @throws \Exception + */ + public function __invoke(ServerRequest $request): Template { $queryParams = $request->getQueryParams(); diff --git a/src/Controller/ClientResetSecretController.php b/src/Controller/Client/ResetSecretController.php similarity index 84% rename from src/Controller/ClientResetSecretController.php rename to src/Controller/Client/ResetSecretController.php index 306b06f0..172ce0f8 100644 --- a/src/Controller/ClientResetSecretController.php +++ b/src/Controller/Client/ResetSecretController.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; - $this->messages = $messages; $this->authContextService = $authContextService; } + /** + * @throws BadRequest + * @throws NotFound + * @throws Exception + * @throws \Exception + */ public function __invoke(ServerRequest $request): RedirectResponse { $client = $this->getClientFromRequest($request); $body = $request->getParsedBody(); - $clientSecret = $body['secret'] ?? null; + $clientSecret = empty($body['secret']) ? null : (string)$body['secret']; if ('POST' === mb_strtoupper($request->getMethod())) { if (!$clientSecret) { diff --git a/src/Controller/ClientShowController.php b/src/Controller/Client/ShowController.php similarity index 70% rename from src/Controller/ClientShowController.php rename to src/Controller/Client/ShowController.php index 2dc5cc8b..bb06e86e 100644 --- a/src/Controller/ClientShowController.php +++ b/src/Controller/Client/ShowController.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; - $this->allowedOriginRepository = $allowedOriginRepository; - $this->templateFactory = $templateFactory; $this->authContextService = $authContextService; } - public function __invoke(ServerRequest $request): \SimpleSAML\XHTML\Template + /** + * @throws BadRequest|Exception|NotFound|OidcServerException|JsonException + */ + public function __invoke(ServerRequest $request): Template { $client = $this->getClientFromRequest($request); $allowedOrigins = $this->allowedOriginRepository->get($client->getIdentifier()); diff --git a/src/Controller/ConfigurationDiscoveryController.php b/src/Controller/ConfigurationDiscoveryController.php new file mode 100644 index 00000000..08f3fea1 --- /dev/null +++ b/src/Controller/ConfigurationDiscoveryController.php @@ -0,0 +1,32 @@ +opMetadataService->getMetadata()); + } +} diff --git a/src/Controller/OpenIdConnectInstallerController.php b/src/Controller/InstallerController.php similarity index 64% rename from src/Controller/OpenIdConnectInstallerController.php rename to src/Controller/InstallerController.php index 6f776e72..63e5b591 100644 --- a/src/Controller/OpenIdConnectInstallerController.php +++ b/src/Controller/InstallerController.php @@ -1,5 +1,7 @@ templateFactory = $templateFactory; - $this->messages = $messages; - $this->databaseMigration = $databaseMigration; - $this->databaseLegacyOAuth2Import = $databaseLegacyOAuth2Import; } /** - * @return \Laminas\Diactoros\Response\RedirectResponse|\SimpleSAML\XHTML\Template + * @throws Exception */ - public function __invoke(ServerRequest $request) + public function __invoke(ServerRequest $request): Template|RedirectResponse { if ($this->databaseMigration->isUpdated()) { return new RedirectResponse((new HTTP())->addURLParameters('admin-clients/index.php', [])); } - $oauth2Enabled = \in_array('oauth2', Module::getModules(), true); + $oauth2Enabled = in_array('oauth2', Module::getModules(), true); $parsedBody = $request->getParsedBody(); if ('POST' === $request->getMethod() && ($parsedBody['migrate'] ?? false)) { diff --git a/src/Controller/OpenIdConnectJwksController.php b/src/Controller/JwksController.php similarity index 66% rename from src/Controller/OpenIdConnectJwksController.php rename to src/Controller/JwksController.php index 4cd13e32..ed951baa 100644 --- a/src/Controller/OpenIdConnectJwksController.php +++ b/src/Controller/JwksController.php @@ -1,5 +1,7 @@ jsonWebKeySetService = $jsonWebKeySetService; } - public function __invoke(ServerRequest $request): JsonResponse + public function __invoke(): JsonResponse { return new JsonResponse([ 'keys' => array_values($this->jsonWebKeySetService->keys()), diff --git a/src/Controller/LogoutController.php b/src/Controller/LogoutController.php index 105cedba..7e000cec 100644 --- a/src/Controller/LogoutController.php +++ b/src/Controller/LogoutController.php @@ -1,19 +1,21 @@ authorizationServer = $authorizationServer; - $this->authenticationService = $authenticationService; - $this->sessionService = $sessionService; - $this->sessionLogoutTicketStoreBuilder = $sessionLogoutTicketStoreBuilder; - $this->loggerService = $loggerService; - $this->templateFactory = $templateFactory; } /** @@ -75,11 +58,13 @@ public function __invoke(ServerRequest $request): Response // If id_token_hint was provided, resolve session ID $idTokenHint = $logoutRequest->getIdTokenHint(); if ($idTokenHint !== null) { - $sidClaim = $idTokenHint->claims()->get('sid'); + $sidClaim = empty($idTokenHint->claims()->get('sid')) ? + null : + (string)$idTokenHint->claims()->get('sid'); } // Check if RP is requesting logout for session that previously existed (not this current session). - // Claim 'sid' from 'id_token_hint' logout parameter indicates for which session should logout be + // Claim 'sid' from 'id_token_hint' logout parameter indicates for which session should log out be // performed (sid is session ID used when ID token was issued during authn). If the requested // sid is different from the current session ID, try to find the requested session. if ( @@ -143,12 +128,14 @@ public static function logoutHandler(): void // Check for session logout tickets. If there are any, it means that the logout was initiated using OIDC RP // initiated flow for specific session (not current one). - $sessionLogoutTicketStore = SessionLogoutTicketStoreBuilder::getStaticInstance(); + $sessionLogoutTicketStore = LogoutTicketStoreBuilder::getStaticInstance(); $sessionLogoutTickets = $sessionLogoutTicketStore->getAll(); if (! empty($sessionLogoutTickets)) { + // TODO low mivanci This could brake since interface does not mandate type. Move to strong typing. + /** @var array $sessionLogoutTicket */ foreach ($sessionLogoutTickets as $sessionLogoutTicket) { - $sid = $sessionLogoutTicket['sid']; + $sid = (string)$sessionLogoutTicket['sid']; if ($sid === $session->getSessionId()) { continue; } @@ -173,17 +160,22 @@ public static function logoutHandler(): void } } - $sessionLogoutTicketStore->deleteMultiple(array_map(fn($slt) => $slt['sid'], $sessionLogoutTickets)); + $sessionLogoutTicketStore->deleteMultiple( + array_map(fn(array $slt): string => (string)$slt['sid'], $sessionLogoutTickets) + ); } (new BackChannelLogoutHandler())->handle($relyingPartyAssociations); } + /** + * @throws ConfigurationError + */ protected function resolveResponse(LogoutRequest $logoutRequest, bool $wasLogoutActionCalled): Response { if (($postLogoutRedirectUri = $logoutRequest->getPostLogoutRedirectUri()) !== null) { if ($logoutRequest->getState() !== null) { - $postLogoutRedirectUri .= (strstr($postLogoutRedirectUri, '?') === false) ? '?' : '&'; + $postLogoutRedirectUri .= (!str_contains($postLogoutRedirectUri, '?')) ? '?' : '&'; $postLogoutRedirectUri .= http_build_query(['state' => $logoutRequest->getState()]); } diff --git a/src/Controller/OpenIdConnectDiscoverConfigurationController.php b/src/Controller/OpenIdConnectDiscoverConfigurationController.php deleted file mode 100644 index 405cdba3..00000000 --- a/src/Controller/OpenIdConnectDiscoverConfigurationController.php +++ /dev/null @@ -1,38 +0,0 @@ -oidcOpenIdProviderMetadataService = $oidcOpenIdProviderMetadataService; - } - - public function __invoke(ServerRequest $serverRequest): JsonResponse - { - return new JsonResponse($this->oidcOpenIdProviderMetadataService->getMetadata()); - } -} diff --git a/src/Controller/Traits/AuthenticatedGetClientFromRequestTrait.php b/src/Controller/Traits/AuthenticatedGetClientFromRequestTrait.php index 8f9927e5..9ca2dacf 100644 --- a/src/Controller/Traits/AuthenticatedGetClientFromRequestTrait.php +++ b/src/Controller/Traits/AuthenticatedGetClientFromRequestTrait.php @@ -1,5 +1,7 @@ getQueryParams(); - $clientId = $params['client_id'] ?? null; + $clientId = empty($params['client_id']) ? null : (string)$params['client_id']; - if (!$clientId) { + if (!is_string($clientId)) { throw new BadRequest('Client id is missing.'); } $authedUser = null; diff --git a/src/Controller/Traits/GetClientFromRequestTrait.php b/src/Controller/Traits/GetClientFromRequestTrait.php index 0e58de04..f375cc36 100644 --- a/src/Controller/Traits/GetClientFromRequestTrait.php +++ b/src/Controller/Traits/GetClientFromRequestTrait.php @@ -1,5 +1,7 @@ getQueryParams(); - $clientId = $params['client_id'] ?? null; + $clientId = empty($params['client_id']) ? null : (string)$params['client_id']; - if (!$clientId) { + if (!is_string($clientId)) { throw new BadRequest('Client id is missing.'); } diff --git a/src/Controller/OpenIdConnectUserInfoController.php b/src/Controller/UserInfoController.php similarity index 70% rename from src/Controller/OpenIdConnectUserInfoController.php rename to src/Controller/UserInfoController.php index 0c657045..8c948617 100644 --- a/src/Controller/OpenIdConnectUserInfoController.php +++ b/src/Controller/UserInfoController.php @@ -1,5 +1,7 @@ resourceServer = $resourceServer; - $this->accessTokenRepository = $accessTokenRepository; - $this->userRepository = $userRepository; - $this->allowedOriginRepository = $allowedOriginRepository; - $this->claimTranslatorExtractor = $claimTranslatorExtractor; } + /** + * @throws UserNotFound + * @throws OidcServerException + * @throws OAuthServerException + */ public function __invoke(ServerRequest $request): Response { // Check if this is actually a CORS preflight request... @@ -79,7 +55,9 @@ public function __invoke(ServerRequest $request): Response $authorization = $this->resourceServer->validateAuthenticatedRequest($request); + /** @var string $tokenId */ $tokenId = $authorization->getAttribute('oauth_access_token_id'); + /** @var string[] $scopes */ $scopes = $authorization->getAttribute('oauth_scopes'); $accessToken = $this->accessTokenRepository->findById($tokenId); @@ -100,18 +78,15 @@ public function __invoke(ServerRequest $request): Response } /** - * @param AccessTokenEntity $accessToken - * + * @throws OidcServerException * @throws UserNotFound - * - * @return UserEntity */ - private function getUser(AccessTokenEntity $accessToken) + private function getUser(AccessTokenEntity $accessToken): UserEntity { $userIdentifier = (string) $accessToken->getUserIdentifier(); $user = $this->userRepository->getUserEntityByIdentifier($userIdentifier); if (!$user instanceof UserEntity) { - throw new UserNotFound("User ${userIdentifier} not found"); + throw new UserNotFound("User $userIdentifier not found"); } return $user; @@ -120,8 +95,6 @@ private function getUser(AccessTokenEntity $accessToken) /** * Handle CORS 'preflight' requests by checking if 'origin' is registered as allowed to make HTTP CORS requests, * typically initiated in browser by JavaScript clients. - * @param ServerRequest $request - * @return Response * @throws OidcServerException */ protected function handleCors(ServerRequest $request): Response diff --git a/src/Entity/AccessTokenEntity.php b/src/Entities/AccessTokenEntity.php similarity index 64% rename from src/Entity/AccessTokenEntity.php rename to src/Entities/AccessTokenEntity.php index 9859e3a2..0a1c4d86 100644 --- a/src/Entity/AccessTokenEntity.php +++ b/src/Entities/AccessTokenEntity.php @@ -1,5 +1,7 @@ ScopeEntity::fromData($scope), $stateScopes); $accessToken->identifier = $state['id']; $accessToken->scopes = $scopes; - $accessToken->expiryDateTime = \DateTimeImmutable::createFromMutable( + $accessToken->expiryDateTime = DateTimeImmutable::createFromMutable( TimestampGenerator::utc($state['expires_at']) ); - $accessToken->userIdentifier = $state['user_id']; + $accessToken->userIdentifier = empty($state['user_id']) ? null : (string)$state['user_id']; $accessToken->client = $state['client']; $accessToken->isRevoked = (bool) $state['is_revoked']; - $accessToken->authCodeId = $state['auth_code_id']; - $accessToken->requestedClaims = json_decode($state['requested_claims'] ?? '[]', true); + $accessToken->authCodeId = empty($state['auth_code_id']) ? null : (string)$state['auth_code_id']; + + $stateRequestedClaims = json_decode( + empty($state['requested_claims']) ? '[]' : (string)$state['requested_claims'], + true, + 512, + JSON_THROW_ON_ERROR + ); + if (!is_array($stateRequestedClaims)) { + throw OidcServerException::serverError('Invalid Access Token Entity state: requested claims'); + } + $accessToken->requestedClaims = $stateRequestedClaims; + return $accessToken; } @@ -116,9 +147,6 @@ public function getRequestedClaims(): array return $this->requestedClaims; } - /** - * @param array $requestedClaims - */ public function setRequestedClaims(array $requestedClaims): void { $this->requestedClaims = $requestedClaims; @@ -126,18 +154,20 @@ public function setRequestedClaims(array $requestedClaims): void /** * {@inheritdoc} + * @throws JsonException + * @throws JsonException */ public function getState(): array { return [ 'id' => $this->getIdentifier(), - 'scopes' => json_encode($this->scopes), + 'scopes' => json_encode($this->scopes, JSON_THROW_ON_ERROR), 'expires_at' => $this->getExpiryDateTime()->format('Y-m-d H:i:s'), 'user_id' => $this->getUserIdentifier(), 'client_id' => $this->getClient()->getIdentifier(), 'is_revoked' => (int) $this->isRevoked(), 'auth_code_id' => $this->getAuthCodeId(), - 'requested_claims' => json_encode($this->requestedClaims) + 'requested_claims' => json_encode($this->requestedClaims, JSON_THROW_ON_ERROR) ]; } @@ -146,14 +176,14 @@ public function getState(): array * @return string * @throws OAuthServerException */ - public function __toString() + public function __toString(): string { return $this->stringRepresentation = $this->convertToJWT()->toString(); } /** * Get string representation of access token at the moment of casting it to string. - * @return string|null String representation or null if it was not casted to string yet. + * @return string|null String representation or null if it was not cast to string yet. */ public function toString(): ?string { @@ -166,6 +196,7 @@ public function toString(): ?string * * @return Token * @throws OAuthServerException + * @throws Exception */ protected function convertToJWT(): Token { @@ -173,7 +204,7 @@ protected function convertToJWT(): Token $jwtBuilder = $jwtBuilderService->getDefaultJwtTokenBuilder() ->permittedFor($this->getClient()->getIdentifier()) - ->identifiedBy($this->getIdentifier()) + ->identifiedBy((string)$this->getIdentifier()) ->issuedAt(new DateTimeImmutable()) ->canOnlyBeUsedAfter(new DateTimeImmutable()) ->expiresAt($this->getExpiryDateTime()) diff --git a/src/Entities/AuthCodeEntity.php b/src/Entities/AuthCodeEntity.php new file mode 100644 index 00000000..ac1feac4 --- /dev/null +++ b/src/Entities/AuthCodeEntity.php @@ -0,0 +1,99 @@ + ScopeEntity::fromData($scope), + $stateScopes + ); + + $authCode->identifier = $state['id']; + $authCode->scopes = $scopes; + $authCode->expiryDateTime = DateTimeImmutable::createFromMutable( + TimestampGenerator::utc($state['expires_at']) + ); + $authCode->userIdentifier = empty($state['user_id']) ? null : (string)$state['user_id']; + $authCode->client = $state['client']; + $authCode->isRevoked = (bool) $state['is_revoked']; + $authCode->redirectUri = empty($state['redirect_uri']) ? null : (string)$state['redirect_uri']; + $authCode->nonce = empty($state['nonce']) ? null : (string)$state['nonce']; + + return $authCode; + } + + /** + * @throws JsonException + */ + public function getState(): array + { + return [ + 'id' => $this->getIdentifier(), + 'scopes' => json_encode($this->scopes, JSON_THROW_ON_ERROR), + 'expires_at' => $this->getExpiryDateTime()->format('Y-m-d H:i:s'), + 'user_id' => $this->getUserIdentifier(), + 'client_id' => $this->client->getIdentifier(), + 'is_revoked' => (int) $this->isRevoked(), + 'redirect_uri' => $this->getRedirectUri(), + 'nonce' => $this->getNonce(), + ]; + } +} diff --git a/src/Entities/ClaimSetEntity.php b/src/Entities/ClaimSetEntity.php new file mode 100644 index 00000000..a384db6e --- /dev/null +++ b/src/Entities/ClaimSetEntity.php @@ -0,0 +1,31 @@ + + * @copyright (c) 2018 Steve Rhoades + * @license http://opensource.org/licenses/MIT MIT + */ +class ClaimSetEntity implements ClaimSetEntityInterface +{ + public function __construct(protected string $scope, protected array $claims) + { + } + + public function getScope(): string + { + return $this->scope; + } + + public function getClaims(): array + { + return $this->claims; + } +} diff --git a/src/Entity/ClientEntity.php b/src/Entities/ClientEntity.php similarity index 61% rename from src/Entity/ClientEntity.php rename to src/Entities/ClientEntity.php index 721a3489..0189d6b4 100644 --- a/src/Entity/ClientEntity.php +++ b/src/Entities/ClientEntity.php @@ -1,5 +1,7 @@ isEnabled = true; } + /** + * @param string $id + * @param string $secret + * @param string $name + * @param string $description + * @param string[] $redirectUri + * @param string[] $scopes + * @param bool $isEnabled + * @param bool $isConfidential + * @param string|null $authSource + * @param string|null $owner + * @param string[] $postLogoutRedirectUri + * @param string|null $backChannelLogoutUri + * @return ClientEntityInterface + */ public static function fromData( string $id, string $secret, @@ -70,43 +94,76 @@ public static function fromData( $client->secret = $secret; $client->name = $name; $client->description = $description; - $client->authSource = $authSource; + $client->authSource = empty($authSource) ? null : $authSource; $client->redirectUri = $redirectUri; $client->scopes = $scopes; $client->isEnabled = $isEnabled; $client->isConfidential = $isConfidential; - $client->owner = $owner; + $client->owner = empty($owner) ? null : $owner; $client->postLogoutRedirectUri = $postLogoutRedirectUri; - $client->backChannelLogoutUri = $backChannelLogoutUri; + $client->backChannelLogoutUri = empty($backChannelLogoutUri) ? null : $backChannelLogoutUri; return $client; } /** - * {@inheritdoc} + * @throws JsonException + * @throws OidcServerException */ public static function fromState(array $state): self { $client = new self(); + if ( + !is_string($state['id']) || + !is_string($state['secret']) || + !is_string($state['name']) || + !is_string($state['redirect_uri']) || + !is_string($state['scopes']) + ) { + throw OidcServerException::serverError('Invalid Client Entity state'); + } + $client->identifier = $state['id']; $client->secret = $state['secret']; $client->name = $state['name']; - $client->description = $state['description']; - $client->authSource = $state['auth_source']; - $client->redirectUri = json_decode($state['redirect_uri'], true); - $client->scopes = json_decode($state['scopes'], true); + $client->description = (string)($state['description'] ?? ''); + $client->authSource = empty($state['auth_source']) ? null : (string)$state['auth_source']; + + /** @var string[] $redirectUris */ + $redirectUris = json_decode($state['redirect_uri'], true, 512, JSON_THROW_ON_ERROR); + $client->redirectUri = $redirectUris; + + /** @var string[] $scopes */ + $scopes = json_decode($state['scopes'], true, 512, JSON_THROW_ON_ERROR); + $client->scopes = $scopes; + $client->isEnabled = (bool) $state['is_enabled']; $client->isConfidential = (bool) ($state['is_confidential'] ?? false); - $client->owner = $state['owner'] ?? null; - $client->postLogoutRedirectUri = json_decode($state['post_logout_redirect_uri'] ?? "[]", true); - $client->backChannelLogoutUri = $state['backchannel_logout_uri'] ?? null; + $client->owner = empty($state['owner']) ? null : (string)$state['owner']; + + /** @var string[] $postLogoutRedirectUris */ + $postLogoutRedirectUris = json_decode( + (string)($state['post_logout_redirect_uri'] ?? "[]"), + true, + 512, + JSON_THROW_ON_ERROR + ); + $client->postLogoutRedirectUri = $postLogoutRedirectUris; + + + $client->backChannelLogoutUri = empty($state['backchannel_logout_uri']) ? + null : + (string)$state['backchannel_logout_uri']; return $client; } /** * {@inheritdoc} + * @throws JsonException + * @throws JsonException + * @throws JsonException */ public function getState(): array { @@ -116,12 +173,12 @@ public function getState(): array 'name' => $this->getName(), 'description' => $this->getDescription(), 'auth_source' => $this->getAuthSourceId(), - 'redirect_uri' => json_encode($this->getRedirectUri()), - 'scopes' => json_encode($this->getScopes()), + 'redirect_uri' => json_encode($this->getRedirectUri(), JSON_THROW_ON_ERROR), + 'scopes' => json_encode($this->getScopes(), JSON_THROW_ON_ERROR), 'is_enabled' => (int) $this->isEnabled(), 'is_confidential' => (int) $this->isConfidential(), 'owner' => $this->getOwner(), - 'post_logout_redirect_uri' => json_encode($this->getPostLogoutRedirectUri()), + 'post_logout_redirect_uri' => json_encode($this->getPostLogoutRedirectUri(), JSON_THROW_ON_ERROR), 'backchannel_logout_uri' => $this->getBackChannelLogoutUri(), ]; diff --git a/src/Entity/Interfaces/AccessTokenEntityInterface.php b/src/Entities/Interfaces/AccessTokenEntityInterface.php similarity index 74% rename from src/Entity/Interfaces/AccessTokenEntityInterface.php rename to src/Entities/Interfaces/AccessTokenEntityInterface.php index d75195e9..960ea44b 100644 --- a/src/Entity/Interfaces/AccessTokenEntityInterface.php +++ b/src/Entities/Interfaces/AccessTokenEntityInterface.php @@ -1,6 +1,8 @@ + * @copyright (c) 2018 Steve Rhoades + * @license http://opensource.org/licenses/MIT MIT + */ +interface ClaimSetEntityInterface extends ClaimSetInterface, ScopeInterface +{ +} diff --git a/src/Entities/Interfaces/ClaimSetInterface.php b/src/Entities/Interfaces/ClaimSetInterface.php new file mode 100644 index 00000000..342f1f34 --- /dev/null +++ b/src/Entities/Interfaces/ClaimSetInterface.php @@ -0,0 +1,17 @@ + + * @copyright (c) 2018 Steve Rhoades + * @license http://opensource.org/licenses/MIT MIT + */ +interface ClaimSetInterface +{ + public function getClaims(): array; +} diff --git a/src/Entity/Interfaces/ClientEntityInterface.php b/src/Entities/Interfaces/ClientEntityInterface.php similarity index 86% rename from src/Entity/Interfaces/ClientEntityInterface.php rename to src/Entities/Interfaces/ClientEntityInterface.php index 89c4581a..3402905d 100644 --- a/src/Entity/Interfaces/ClientEntityInterface.php +++ b/src/Entities/Interfaces/ClientEntityInterface.php @@ -1,11 +1,17 @@ + * @copyright (c) 2018 Steve Rhoades + * @license http://opensource.org/licenses/MIT MIT + */ +interface ScopeInterface +{ + public function getScope(): string; +} diff --git a/src/Entity/Interfaces/TokenAssociatableWithAuthCodeInterface.php b/src/Entities/Interfaces/TokenAssociatableWithAuthCodeInterface.php similarity index 79% rename from src/Entity/Interfaces/TokenAssociatableWithAuthCodeInterface.php rename to src/Entities/Interfaces/TokenAssociatableWithAuthCodeInterface.php index 9f544e1f..29306133 100644 --- a/src/Entity/Interfaces/TokenAssociatableWithAuthCodeInterface.php +++ b/src/Entities/Interfaces/TokenAssociatableWithAuthCodeInterface.php @@ -1,6 +1,8 @@ identifier = $state['id']; - $refreshToken->expiryDateTime = \DateTimeImmutable::createFromMutable( + $refreshToken->expiryDateTime = DateTimeImmutable::createFromMutable( TimestampGenerator::utc($state['expires_at']) ); $refreshToken->accessToken = $state['access_token']; $refreshToken->isRevoked = (bool) $state['is_revoked']; - $refreshToken->authCodeId = $state['auth_code_id']; + $refreshToken->authCodeId = empty($state['auth_code_id']) ? null : (string)$state['auth_code_id']; return $refreshToken; } diff --git a/src/Entity/ScopeEntity.php b/src/Entities/ScopeEntity.php similarity index 76% rename from src/Entity/ScopeEntity.php rename to src/Entities/ScopeEntity.php index 84a061b6..d9ec406d 100644 --- a/src/Entity/ScopeEntity.php +++ b/src/Entities/ScopeEntity.php @@ -1,5 +1,7 @@ */ - private $attributes; + private array $claims; - /** - * Constructor. - */ private function __construct() { } /** - * @param array $attributes + * @param array $claims */ public static function fromData( string $identifier, string $description = null, string $icon = null, - array $attributes = [] + array $claims = [] ): self { $scope = new self(); $scope->identifier = $identifier; $scope->description = $description; $scope->icon = $icon; - $scope->attributes = $attributes; + $scope->claims = $claims; return $scope; } @@ -75,13 +77,13 @@ public function getDescription(): ?string /** * @return array */ - public function getAttributes(): array + public function getClaims(): array { - return $this->attributes; + return $this->claims; } public function jsonSerialize(): string { - return $this->getIdentifier(); + return (string) $this->getIdentifier(); } } diff --git a/src/Entity/Traits/AssociateWithAuthCodeTrait.php b/src/Entities/Traits/AssociateWithAuthCodeTrait.php similarity index 66% rename from src/Entity/Traits/AssociateWithAuthCodeTrait.php rename to src/Entities/Traits/AssociateWithAuthCodeTrait.php index ece8373c..bb481822 100644 --- a/src/Entity/Traits/AssociateWithAuthCodeTrait.php +++ b/src/Entities/Traits/AssociateWithAuthCodeTrait.php @@ -1,13 +1,12 @@ nonce; } - /** - * @inheritDoc - */ - public function setNonce($nonce): void + public function setNonce(string $nonce): void { $this->nonce = $nonce; } diff --git a/src/Entity/Traits/RevokeTokenTrait.php b/src/Entities/Traits/RevokeTokenTrait.php similarity index 82% rename from src/Entity/Traits/RevokeTokenTrait.php rename to src/Entities/Traits/RevokeTokenTrait.php index fabe2b53..2834a02f 100644 --- a/src/Entity/Traits/RevokeTokenTrait.php +++ b/src/Entities/Traits/RevokeTokenTrait.php @@ -1,5 +1,7 @@ identifier = $state['id']; - $user->claims = json_decode($state['claims'], true, 512, JSON_INVALID_UTF8_SUBSTITUTE); + $claims = json_decode($state['claims'], true, 512, JSON_INVALID_UTF8_SUBSTITUTE); + + if (!is_array($claims)) { + throw OidcServerException::serverError('Invalid user entity data'); + } + $user->claims = $claims; $user->updatedAt = TimestampGenerator::utc($state['updated_at']); $user->createdAt = TimestampGenerator::utc($state['created_at']); @@ -98,6 +122,9 @@ public function getClaims(): array return $this->claims; } + /** + * @throws Exception + */ public function setClaims(array $claims): self { $this->claims = $claims; @@ -106,12 +133,12 @@ public function setClaims(array $claims): self return $this; } - public function getUpdatedAt(): \DateTime + public function getUpdatedAt(): DateTime { return $this->updatedAt; } - public function getCreatedAt(): \DateTime + public function getCreatedAt(): DateTime { return $this->createdAt; } diff --git a/src/Entity/AuthCodeEntity.php b/src/Entity/AuthCodeEntity.php deleted file mode 100644 index b18536e6..00000000 --- a/src/Entity/AuthCodeEntity.php +++ /dev/null @@ -1,74 +0,0 @@ -identifier = $state['id']; - $authCode->scopes = $scopes; - $authCode->expiryDateTime = \DateTimeImmutable::createFromMutable( - TimestampGenerator::utc($state['expires_at']) - ); - $authCode->userIdentifier = $state['user_id']; - $authCode->client = $state['client']; - $authCode->isRevoked = (bool) $state['is_revoked']; - $authCode->redirectUri = $state['redirect_uri']; - $authCode->nonce = $state['nonce']; - - - return $authCode; - } - - public function getState(): array - { - return [ - 'id' => $this->getIdentifier(), - 'scopes' => json_encode($this->scopes), - 'expires_at' => $this->getExpiryDateTime()->format('Y-m-d H:i:s'), - 'user_id' => $this->getUserIdentifier(), - 'client_id' => $this->client->getIdentifier(), - 'is_revoked' => (int) $this->isRevoked(), - 'redirect_uri' => $this->getRedirectUri(), - 'nonce' => $this->getNonce(), - ]; - } -} diff --git a/src/Factories/AuthSimpleFactory.php b/src/Factories/AuthSimpleFactory.php index 3f9d723f..86b1ed6e 100644 --- a/src/Factories/AuthSimpleFactory.php +++ b/src/Factories/AuthSimpleFactory.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; - $this->configurationService = $configurationService; } /** @@ -58,8 +56,6 @@ public function getDefaultAuthSource(): Simple /** * Get auth source defined on the client. If not set on the client, get the default auth source defined in config. * - * @param ClientEntityInterface $client - * @return string * @throws Exception */ public function resolveAuthSourceId(ClientEntityInterface $client): string @@ -72,6 +68,6 @@ public function resolveAuthSourceId(ClientEntityInterface $client): string */ public function getDefaultAuthSourceId(): string { - return $this->configurationService->getOpenIDConnectConfiguration()->getString('auth'); + return $this->moduleConfig->config()->getString(ModuleConfig::OPTION_AUTH_SOURCE); } } diff --git a/src/Factories/AuthorizationServerFactory.php b/src/Factories/AuthorizationServerFactory.php index 76b43c62..957e9fc8 100644 --- a/src/Factories/AuthorizationServerFactory.php +++ b/src/Factories/AuthorizationServerFactory.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; - $this->accessTokenRepository = $accessTokenRepository; - $this->scopeRepository = $scopeRepository; - $this->authCodeGrant = $authCodeGrant; - $this->oAuth2ImplicitGrant = $oAuth2ImplicitGrant; - $this->implicitGrant = $implicitGrant; - $this->refreshTokenGrant = $refreshTokenGrant; - $this->accessTokenDuration = $accessTokenDuration; - $this->idTokenResponse = $idTokenResponse; - $this->requestRulesManager = $requestRulesManager; - $this->privateKey = $privateKey; - $this->encryptionKey = $encryptionKey; } public function build(): AuthorizationServer diff --git a/src/Factories/ClaimTranslatorExtractorFactory.php b/src/Factories/ClaimTranslatorExtractorFactory.php index af96b7c0..762bf29f 100644 --- a/src/Factories/ClaimTranslatorExtractorFactory.php +++ b/src/Factories/ClaimTranslatorExtractorFactory.php @@ -1,5 +1,7 @@ configurationService = $configurationService; + public function __construct(private readonly ModuleConfig $moduleConfig) + { } /** - * @throws InvalidArgumentException * @throws Exception */ public function build(): ClaimTranslatorExtractor { - $translatorTable = $this->configurationService->getOpenIDConnectConfiguration() - ->getOptionalArray('translate', []); + $translatorTable = $this->moduleConfig->config() + ->getOptionalArray(ModuleConfig::OPTION_AUTH_SAML_TO_OIDC_TRANSLATE_TABLE, []); - $privateScopes = $this->configurationService->getOpenIDPrivateScopes(); + $privateScopes = $this->moduleConfig->getOpenIDPrivateScopes(); $claimSet = []; $allowedMultipleValueClaims = []; + /** + * @var string $scopeName + * @var array $scopeConfig + */ foreach ($privateScopes as $scopeName => $scopeConfig) { - $claims = $scopeConfig['claims'] ?? []; + $claims = is_array($scopeConfig['claims']) ? $scopeConfig['claims'] : []; if ($this->isScopeClaimNamePrefixSet($scopeConfig)) { - $prefix = $scopeConfig[self::CONFIG_KEY_CLAIM_NAME_PREFIX]; + $prefix = (string)($scopeConfig[self::CONFIG_KEY_CLAIM_NAME_PREFIX] ?? ''); $translatorTable = $this->applyPrefixToTranslatorTableKeys($translatorTable, $claims, $prefix); $claims = $this->applyPrefixToClaimNames($claims, $prefix); @@ -65,7 +65,7 @@ public function build(): ClaimTranslatorExtractor } } - $userIdAttr = $this->configurationService->getOpenIDConnectConfiguration()->getString('useridattr'); + $userIdAttr = $this->moduleConfig->getUserIdentifierAttribute(); return new ClaimTranslatorExtractor($userIdAttr, $claimSet, $translatorTable, $allowedMultipleValueClaims); } @@ -80,6 +80,10 @@ public function build(): ClaimTranslatorExtractor */ protected function applyPrefixToTranslatorTableKeys(array $translatorTable, array $claims, string $prefix): array { + /** + * @var string $claimKey + * @var array $mapping + */ foreach ($translatorTable as $claimKey => $mapping) { if (in_array($claimKey, $claims)) { $prefixedClaimKey = $prefix . $claimKey; @@ -98,7 +102,7 @@ protected function applyPrefixToTranslatorTableKeys(array $translatorTable, arra */ protected function applyPrefixToClaimNames(array $claims, string $prefix): array { - array_walk($claims, function (&$value, $key, $prefix) { + array_walk($claims, function (string &$value, mixed $key, string $prefix) { $value = $prefix . $value; }, $prefix); @@ -107,24 +111,20 @@ protected function applyPrefixToClaimNames(array $claims, string $prefix): array /** * Check if the scope has a claim name prefix set - * @param array $scopeConfig - * @return bool */ protected function isScopeClaimNamePrefixSet(array $scopeConfig): bool { return isset($scopeConfig[self::CONFIG_KEY_CLAIM_NAME_PREFIX]) && - is_string($scopeConfig[self::CONFIG_KEY_CLAIM_NAME_PREFIX]) && - !empty($scopeConfig[self::CONFIG_KEY_CLAIM_NAME_PREFIX]); + is_string($scopeConfig[self::CONFIG_KEY_CLAIM_NAME_PREFIX]) && + !empty($scopeConfig[self::CONFIG_KEY_CLAIM_NAME_PREFIX]); } /** * Check if the scope allows claims to have multiple values. - * @param array $scopeConfig - * @return bool */ protected function doesScopeAllowMultipleClaimValues(array $scopeConfig): bool { return isset($scopeConfig[self::CONFIG_KEY_MULTIPLE_CLAIM_VALUES_ALLOWED]) && - boolval($scopeConfig[self::CONFIG_KEY_MULTIPLE_CLAIM_VALUES_ALLOWED]); + $scopeConfig[self::CONFIG_KEY_MULTIPLE_CLAIM_VALUES_ALLOWED]; } } diff --git a/src/Factories/CryptKeyFactory.php b/src/Factories/CryptKeyFactory.php index bc63a3ed..9ed903ac 100644 --- a/src/Factories/CryptKeyFactory.php +++ b/src/Factories/CryptKeyFactory.php @@ -1,41 +1,35 @@ publicKeyPath = $publicKeyPath; - $this->privateKeyPath = $privateKeyPath; - $this->passPhrase = $passPhrase; } + /** + * @throws \Exception + */ public function buildPrivateKey(): CryptKey { - return new CryptKey($this->privateKeyPath, $this->passPhrase); + return new CryptKey( + $this->moduleConfig->getPrivateKeyPath(), + $this->moduleConfig->getPrivateKeyPassPhrase() + ); } + /** + * @throws \Exception + */ public function buildPublicKey(): CryptKey { - return new CryptKey($this->publicKeyPath); + return new CryptKey($this->moduleConfig->getCertPath()); } } diff --git a/src/Factories/FormFactory.php b/src/Factories/FormFactory.php index 95de80b9..9e3fc04f 100644 --- a/src/Factories/FormFactory.php +++ b/src/Factories/FormFactory.php @@ -1,5 +1,7 @@ configurationService = $configurationService; } /** - * @param string $classname Form classname + * @param class-string $classname Form classname * * @throws \Exception * * @return mixed */ - public function build(string $classname) + public function build(string $classname): mixed { - if (!class_exists($classname) && ($classname instanceof Form)) { - throw new Exception("Invalid form: {$classname}"); + if (!is_a($classname, Form::class, true)) { + throw new Exception("Invalid form: $classname"); } - /** @psalm-suppress InvalidStringClass */ - return new $classname($this->configurationService); + /** @psalm-suppress UnsafeInstantiation */ + return new $classname($this->moduleConfig); } } diff --git a/src/Factories/Grant/AuthCodeGrantFactory.php b/src/Factories/Grant/AuthCodeGrantFactory.php index 2aa2566b..a3ab82d5 100644 --- a/src/Factories/Grant/AuthCodeGrantFactory.php +++ b/src/Factories/Grant/AuthCodeGrantFactory.php @@ -1,5 +1,7 @@ authCodeRepository = $authCodeRepository; - $this->accessTokenRepository = $accessTokenRepository; - $this->refreshTokenRepository = $refreshTokenRepository; - $this->refreshTokenDuration = $refreshTokenDuration; - $this->authCodeDuration = $authCodeDuration; - $this->requestRulesManager = $requestRulesManager; - $this->configurationService = $configurationService; } + /** + * @throws Exception + */ public function build(): AuthCodeGrant { $authCodeGrant = new AuthCodeGrant( @@ -79,8 +46,7 @@ public function build(): AuthCodeGrant $this->accessTokenRepository, $this->refreshTokenRepository, $this->authCodeDuration, - $this->requestRulesManager, - $this->configurationService + $this->requestRulesManager ); $authCodeGrant->setRefreshTokenTTL($this->refreshTokenDuration); diff --git a/src/Factories/Grant/ImplicitGrantFactory.php b/src/Factories/Grant/ImplicitGrantFactory.php index 5cc07314..6648239d 100644 --- a/src/Factories/Grant/ImplicitGrantFactory.php +++ b/src/Factories/Grant/ImplicitGrantFactory.php @@ -1,5 +1,7 @@ idTokenBuilder = $idTokenBuilder; - $this->accessTokenDuration = $accessTokenDuration; - $this->requestRulesManager = $requestRulesManager; - $this->accessTokenDuration = $accessTokenDuration; - $this->accessTokenRepository = $accessTokenRepository; } public function build(): ImplicitGrant diff --git a/src/Factories/Grant/OAuth2ImplicitGrantFactory.php b/src/Factories/Grant/OAuth2ImplicitGrantFactory.php index 46e2027f..f22fb53c 100644 --- a/src/Factories/Grant/OAuth2ImplicitGrantFactory.php +++ b/src/Factories/Grant/OAuth2ImplicitGrantFactory.php @@ -1,5 +1,7 @@ accessTokenDuration = $accessTokenDuration; - $this->requestRulesManager = $requestRulesManager; + public function __construct( + private readonly DateInterval $accessTokenDuration, + private readonly RequestRulesManager $requestRulesManager + ) { } public function build(): OAuth2ImplicitGrant diff --git a/src/Factories/Grant/RefreshTokenGrantFactory.php b/src/Factories/Grant/RefreshTokenGrantFactory.php index 820eb34b..5bfce3bd 100644 --- a/src/Factories/Grant/RefreshTokenGrantFactory.php +++ b/src/Factories/Grant/RefreshTokenGrantFactory.php @@ -1,5 +1,7 @@ refreshTokenRepository = $refreshTokenRepository; - $this->refreshTokenDuration = $refreshTokenDuration; } public function build(): RefreshTokenGrant diff --git a/src/Factories/IdTokenResponseFactory.php b/src/Factories/IdTokenResponseFactory.php index 834dc308..4c71b0bf 100644 --- a/src/Factories/IdTokenResponseFactory.php +++ b/src/Factories/IdTokenResponseFactory.php @@ -1,5 +1,7 @@ userRepository = $userRepository; - $this->configurationService = $configurationService; - $this->idTokenBuilder = $idTokenBuilder; - $this->privateKey = $privateKey; - $this->encryptionKey = $encryptionKey; } public function build(): IdTokenResponse { $idTokenResponse = new IdTokenResponse( $this->userRepository, - $this->configurationService, - $this->idTokenBuilder + $this->idTokenBuilder, + $this->privateKey ); - $idTokenResponse->setPrivateKey($this->privateKey); $idTokenResponse->setEncryptionKey($this->encryptionKey); return $idTokenResponse; diff --git a/src/Factories/ResourceServerFactory.php b/src/Factories/ResourceServerFactory.php index 3481e322..909d2e25 100644 --- a/src/Factories/ResourceServerFactory.php +++ b/src/Factories/ResourceServerFactory.php @@ -1,5 +1,7 @@ accessTokenRepository = $accessTokenRepository; - $this->publicKey = $publicKey; - $this->authorizationValidator = $authorizationValidator; } public function build(): ResourceServer diff --git a/src/Factories/TemplateFactory.php b/src/Factories/TemplateFactory.php index 1084f1d9..3eda36f1 100644 --- a/src/Factories/TemplateFactory.php +++ b/src/Factories/TemplateFactory.php @@ -1,5 +1,7 @@ toArray(); + // TODO mivanci check if this is really necessary anymore $config['usenewui'] = true; $this->configuration = new Configuration($config, 'oidc'); } + /** + * @throws ConfigurationError + */ public function render(string $templateName, array $data = []): Template { $template = new Template($this->configuration, $templateName); diff --git a/src/Form/ClientForm.php b/src/Forms/ClientForm.php similarity index 65% rename from src/Form/ClientForm.php rename to src/Forms/ClientForm.php index 532498cb..5387e316 100644 --- a/src/Form/ClientForm.php +++ b/src/Forms/ClientForm.php @@ -1,5 +1,7 @@ configurationService = $configurationService; - $this->buildForm(); } @@ -59,9 +59,10 @@ public function validateRedirectUri(Form $form): void { /** @var array $values */ $values = $form->getValues(self::TYPE_ARRAY); - + /** @var string[] $redirectUris */ + $redirectUris = $values['redirect_uri'] ?? []; $this->validateByMatchingRegex( - $values['redirect_uri'] ?? [], + $redirectUris, self::REGEX_URI, 'Invalid URI: ' ); @@ -71,9 +72,10 @@ public function validateAllowedOrigin(Form $form): void { /** @var array $values */ $values = $form->getValues(self::TYPE_ARRAY); - + /** @var string[] $allowedOrigins */ + $allowedOrigins = $values['allowed_origin'] ?? []; $this->validateByMatchingRegex( - $values['allowed_origin'] ?? [], + $allowedOrigins, self::REGEX_ALLOWED_ORIGIN_URL, 'Invalid allowed origin: ' ); @@ -83,9 +85,10 @@ public function validatePostLogoutRedirectUri(Form $form): void { /** @var array $values */ $values = $form->getValues(self::TYPE_ARRAY); - + /** @var string[] $postLogoutRedirectUris */ + $postLogoutRedirectUris = $values['post_logout_redirect_uri'] ?? []; $this->validateByMatchingRegex( - $values['post_logout_redirect_uri'] ?? [], + $postLogoutRedirectUris, self::REGEX_URI, 'Invalid post-logout redirect URI: ' ); @@ -93,7 +96,9 @@ public function validatePostLogoutRedirectUri(Form $form): void public function validateBackChannelLogoutUri(Form $form): void { - if (($bclUri = $form->getValues()['backchannel_logout_uri'] ?? null) !== null) { + /** @var ?string $bclUri */ + $bclUri = $form->getValues()['backchannel_logout_uri'] ?? null; + if ($bclUri !== null) { $this->validateByMatchingRegex( [$bclUri], self::REGEX_HTTP_URI, @@ -103,10 +108,8 @@ public function validateBackChannelLogoutUri(Form $form): void } /** - * @param array $values + * @param string[] $values * @param non-empty-string $regex - * @param string $messagePrefix - * @return void */ protected function validateByMatchingRegex( array $values, @@ -126,22 +129,24 @@ public function getValues($returnType = null, ?array $controls = null): array $values = parent::getValues(self::TYPE_ARRAY); // Sanitize redirect_uri and allowed_origin - $values['redirect_uri'] = $this->convertTextToArrayWithLinesAsValues($values['redirect_uri']); + $values['redirect_uri'] = $this->convertTextToArrayWithLinesAsValues((string)$values['redirect_uri']); if (! $values['is_confidential'] && isset($values['allowed_origin'])) { - $values['allowed_origin'] = $this->convertTextToArrayWithLinesAsValues($values['allowed_origin']); + $values['allowed_origin'] = $this->convertTextToArrayWithLinesAsValues((string)$values['allowed_origin']); } else { $values['allowed_origin'] = []; } $values['post_logout_redirect_uri'] = - $this->convertTextToArrayWithLinesAsValues($values['post_logout_redirect_uri']); + $this->convertTextToArrayWithLinesAsValues((string)$values['post_logout_redirect_uri']); - $bclUri = trim($values['backchannel_logout_uri']); + $bclUri = trim((string)$values['backchannel_logout_uri']); $values['backchannel_logout_uri'] = empty($bclUri) ? null : $bclUri; + $scopes = is_array($values['scopes']) ? $values['scopes'] : []; + // openid scope is mandatory $values['scopes'] = array_unique( array_merge( - $values['scopes'], + $scopes, ['openid'] ) ); @@ -149,44 +154,53 @@ public function getValues($returnType = null, ?array $controls = null): array return $values; } - public function setDefaults($data, bool $erase = false) + /** + * @throws Exception + */ + public function setDefaults($data, bool $erase = false): ClientForm { if (! is_array($data)) { - if ($data instanceof \Traversable) { + if ($data instanceof Traversable) { $data = iterator_to_array($data); } else { $data = (array) $data; } } - $data['redirect_uri'] = implode("\n", $data['redirect_uri']); + /** @var string[] $redirectUris */ + $redirectUris = is_array($data['redirect_uri']) ? $data['redirect_uri'] : []; + $data['redirect_uri'] = implode("\n", $redirectUris); // Allowed origins are only available for public clients (not for confidential clients). if (! $data['is_confidential'] && isset($data['allowed_origin'])) { - $data['allowed_origin'] = implode("\n", $data['allowed_origin']); + /** @var string[] $allowedOrigins */ + $allowedOrigins = is_array($data['allowed_origin']) ? $data['allowed_origin'] : []; + $data['allowed_origin'] = implode("\n", $allowedOrigins); } else { $data['allowed_origin'] = ''; } - $data['post_logout_redirect_uri'] = implode("\n", $data['post_logout_redirect_uri']); + /** @var string[] $postLogoutRedirectUris */ + $postLogoutRedirectUris = is_array($data['post_logout_redirect_uri']) ? $data['post_logout_redirect_uri'] : []; + $data['post_logout_redirect_uri'] = implode("\n", $postLogoutRedirectUris); - $data['scopes'] = array_intersect($data['scopes'], array_keys($this->getScopes())); + $scopes = is_array($data['scopes']) ? $data['scopes'] : []; + $data['scopes'] = array_intersect($scopes, array_keys($this->getScopes())); return parent::setDefaults($data, $erase); } + /** + * @throws Exception + */ protected function buildForm(): void { $this->getElementPrototype()->addAttributes(['class' => 'ui form']); - /** @psalm-suppress InvalidPropertyAssignmentValue According to docs this is fine. */ - $this->onValidate[] = [$this, 'validateRedirectUri']; - /** @psalm-suppress InvalidPropertyAssignmentValue According to docs this is fine. */ - $this->onValidate[] = [$this, 'validateAllowedOrigin']; - /** @psalm-suppress InvalidPropertyAssignmentValue According to docs this is fine. */ - $this->onValidate[] = [$this, 'validatePostLogoutRedirectUri']; - /** @psalm-suppress InvalidPropertyAssignmentValue According to docs this is fine. */ - $this->onValidate[] = [$this, 'validateBackChannelLogoutUri']; + $this->onValidate[] = $this->validateRedirectUri(...); + $this->onValidate[] = $this->validateAllowedOrigin(...); + $this->onValidate[] = $this->validatePostLogoutRedirectUri(...); + $this->onValidate[] = $this->validateBackChannelLogoutUri(...); $this->setMethod('POST'); $this->addComponent(new CsrfProtection('{oidc:client:csrf_error}'), Form::PROTECTOR_ID); @@ -223,29 +237,25 @@ protected function buildForm(): void $this->addText('backchannel_logout_uri', '{oidc:client:backchannel_logout_uri}'); } + /** + * @throws Exception + */ protected function getScopes(): array { - return array_map(function ($item) { - return $item['description']; - }, $this->configurationService->getOpenIDScopes()); + return array_map( + fn(array $item): mixed => $item['description'], + $this->moduleConfig->getOpenIDScopes() + ); } /** - * @param string $text * @return string[] */ protected function convertTextToArrayWithLinesAsValues(string $text): array { return array_filter( preg_split("/[\t\r\n]+/", $text), - /** - * @param string $line - * - * @return bool - */ - function (string $line) { - return !empty(trim($line)); - } + fn(string $line): bool => !empty(trim($line)) ); } } diff --git a/src/Form/Controls/CsrfProtection.php b/src/Forms/Controls/CsrfProtection.php similarity index 71% rename from src/Form/Controls/CsrfProtection.php rename to src/Forms/Controls/CsrfProtection.php index 24e8054e..7d9afb28 100644 --- a/src/Form/Controls/CsrfProtection.php +++ b/src/Forms/Controls/CsrfProtection.php @@ -1,5 +1,7 @@ setOmitted() @@ -44,9 +55,12 @@ public function __construct($errorMessage) $this->sspSession = Session::getSessionFromRequest(); } + /** + * @throws Exception + */ public function getToken(): string { - $token = $this->sspSession->getData('form_csrf', 'token'); + $token = (string)$this->sspSession->getData('form_csrf', 'token'); if (!$token) { $token = Random::generate(); diff --git a/src/Services/ConfigurationService.php b/src/ModuleConfig.php similarity index 55% rename from src/Services/ConfigurationService.php rename to src/ModuleConfig.php index 15ae9a74..8b5cee2f 100644 --- a/src/Services/ConfigurationService.php +++ b/src/ModuleConfig.php @@ -1,5 +1,7 @@ [ 'description' => 'openid', ], @@ -49,40 +78,59 @@ class ConfigurationService ]; /** - * @throws ConfigurationError + * @var Configuration Module configuration instance created form module config file. */ - public function __construct() - { - $this->validateConfiguration(); + private readonly Configuration $moduleConfig; + /** + * @var Configuration SimpleSAMLphp configuration instance. + */ + private readonly Configuration $sspConfig; + + /** + * @throws Exception + */ + public function __construct( + string $fileName = self::DEFAULT_FILE_NAME, // Primarily used for easy (unit) testing overrides. + array $overrides = [] // Primarily used for easy (unit) testing overrides. + ) { + $this->moduleConfig = Configuration::loadFromArray( + array_merge(Configuration::getConfig($fileName)->toArray(), $overrides) + ); + + $this->sspConfig = Configuration::getInstance(); + + $this->validate(); } /** * @throws Exception */ - public function getSimpleSAMLConfiguration(): Configuration + public function sspConfig(): Configuration { - return Configuration::getInstance(); + return $this->sspConfig; } /** * @throws Exception */ - public function getOpenIDConnectConfiguration(): Configuration + public function config(): Configuration { - return Configuration::getConfig('module_oidc.php'); + return $this->moduleConfig; } public function getSimpleSAMLSelfURLHost(): string { + // TODO mivanci Create bridge to SSP utility classes return (new HTTP())->getSelfURLHost(); } public function getOpenIdConnectModuleURL(string $path = null): string { + // TODO mivanci Create bridge to SSP utility classes $base = Module::getModuleURL('oidc'); if ($path) { - $base .= "/{$path}"; + $base .= "/$path"; } return $base; @@ -101,7 +149,7 @@ public function getOpenIDScopes(): array */ public function getOpenIDPrivateScopes(): array { - return $this->getOpenIDConnectConfiguration()->getOptionalArray('scopes', []); + return $this->config()->getOptionalArray(self::OPTION_AUTH_CUSTOM_SCOPES, []); } /** @@ -110,7 +158,7 @@ public function getOpenIDPrivateScopes(): array * * @throws ConfigurationError */ - private function validateConfiguration() + private function validate(): void { $privateScopes = $this->getOpenIDPrivateScopes(); array_walk( @@ -120,10 +168,16 @@ private function validateConfiguration() */ function (array $scope, string $name): void { if (in_array($name, array_keys(self::$standardClaims), true)) { - throw new ConfigurationError('Can not overwrite protected scope: ' . $name, 'oidc_config.php'); + throw new ConfigurationError( + 'Can not overwrite protected scope: ' . $name, + self::DEFAULT_FILE_NAME + ); } if (!array_key_exists('description', $scope)) { - throw new ConfigurationError('Scope [' . $name . '] description not defined', 'module_oidc.php'); + throw new ConfigurationError( + 'Scope [' . $name . '] description not defined', + self::DEFAULT_FILE_NAME + ); } } ); @@ -147,6 +201,7 @@ function (array $scope, string $name): void { 'values containing supported ACRs for each auth source key.'); } + /** @psalm-suppress MixedAssignment */ foreach ($acrValues as $acrValue) { if (! is_string($acrValue)) { throw new ConfigurationError('Config option authSourcesToAcrValuesMap should have array ' . @@ -177,7 +232,10 @@ function (array $scope, string $name): void { public function getSigner(): Signer { /** @psalm-var class-string $signerClassname */ - $signerClassname = $this->getOpenIDConnectConfiguration()->getOptionalString('signer', Sha256::class); + $signerClassname = $this->config()->getOptionalString( + self::OPTION_TOKEN_SIGNER, + Sha256::class + ); $class = new ReflectionClass($signerClassname); $signer = $class->newInstance(); @@ -192,23 +250,37 @@ public function getSigner(): Signer /** * Return the path to the public certificate * @return string The file system path + * @throws Exception */ public function getCertPath(): string { - $certName = $this->getOpenIDConnectConfiguration()->getOptionalString('certificate', 'oidc_module.crt'); + $certName = $this->config()->getOptionalString( + self::OPTION_PKI_CERTIFICATE_FILENAME, + self::DEFAULT_PKI_CERTIFICATE_FILENAME + ); return (new Config())->getCertPath($certName); } /** * Get the path to the private key - * @return string + * @throws Exception */ public function getPrivateKeyPath(): string { - $keyName = $this->getOpenIDConnectConfiguration()->getOptionalString('privatekey', 'oidc_module.key'); + $keyName = $this->config()->getOptionalString( + self::OPTION_PKI_PRIVATE_KEY_FILENAME, + self::DEFAULT_PKI_PRIVATE_KEY_FILENAME + ); + // TODO mivanci move to bridge classes to SSP utils return (new Config())->getCertPath($keyName); } + public function getEncryptionKey(): string + { + // TODO mivanci move to bridge classes to SSP utils + return (new Config())->getSecretSalt(); + } + /** * Get the path to the private key * @return ?string @@ -216,7 +288,7 @@ public function getPrivateKeyPath(): string */ public function getPrivateKeyPassPhrase(): ?string { - return $this->getOpenIDConnectConfiguration()->getOptionalString('pass_phrase', null); + return $this->config()->getOptionalString(self::OPTION_PKI_PRIVATE_KEY_PASSPHRASE, null); } /** @@ -227,7 +299,7 @@ public function getPrivateKeyPassPhrase(): ?string */ public function getAuthProcFilters(): array { - return $this->getOpenIDConnectConfiguration()->getOptionalArray('authproc.oidc', []); + return $this->config()->getOptionalArray(self::OPTION_AUTH_PROCESSING_FILTERS, []); } /** @@ -238,7 +310,7 @@ public function getAuthProcFilters(): array */ public function getAcrValuesSupported(): array { - return array_values($this->getOpenIDConnectConfiguration()->getOptionalArray('acrValuesSupported', [])); + return array_values($this->config()->getOptionalArray(self::OPTION_AUTH_ACR_VALUES_SUPPORTED, [])); } /** @@ -249,7 +321,7 @@ public function getAcrValuesSupported(): array */ public function getAuthSourcesToAcrValuesMap(): array { - return $this->getOpenIDConnectConfiguration()->getOptionalArray('authSourcesToAcrValuesMap', []); + return $this->config()->getOptionalArray(self::OPTION_AUTH_SOURCES_TO_ACR_VALUES_MAP, []); } /** @@ -258,8 +330,9 @@ public function getAuthSourcesToAcrValuesMap(): array */ public function getForcedAcrValueForCookieAuthentication(): ?string { - $value = $this->getOpenIDConnectConfiguration() - ->getOptionalValue('forcedAcrValueForCookieAuthentication', null); + /** @psalm-suppress MixedAssignment */ + $value = $this->config() + ->getOptionalValue(self::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION, null); if (is_null($value)) { return null; @@ -267,4 +340,12 @@ public function getForcedAcrValueForCookieAuthentication(): ?string return (string) $value; } + + /** + * @throws Exception + */ + public function getUserIdentifierAttribute(): string + { + return $this->config()->getString(ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE); + } } diff --git a/src/Repositories/AbstractDatabaseRepository.php b/src/Repositories/AbstractDatabaseRepository.php index 5e9744d6..1c62ad82 100644 --- a/src/Repositories/AbstractDatabaseRepository.php +++ b/src/Repositories/AbstractDatabaseRepository.php @@ -1,5 +1,7 @@ config = Configuration::getOptionalConfig('module_oidc.php'); + $this->config = $this->moduleConfig->config(); + // TODO mivanci move to Doctrine DBAL stores $this->database = Database::getInstance(); - $this->configurationService = $configurationService; } - /** - * @return string|null - */ - abstract public function getTableName(); + abstract public function getTableName(): ?string; } diff --git a/src/Repositories/AccessTokenRepository.php b/src/Repositories/AccessTokenRepository.php index 7115b266..2aef8f19 100644 --- a/src/Repositories/AccessTokenRepository.php +++ b/src/Repositories/AccessTokenRepository.php @@ -1,5 +1,7 @@ database->applyPrefix(self::TABLE_NAME); @@ -47,13 +50,21 @@ public function getNewToken( string $authCodeId = null, array $requestedClaims = null ): AccessTokenEntityInterface { + if (!is_null($userIdentifier)) { + $userIdentifier = (string)$userIdentifier; + } + if (empty($userIdentifier)) { + $userIdentifier = null; + } return AccessTokenEntity::fromData($clientEntity, $scopes, $userIdentifier, $authCodeId, $requestedClaims); } /** * {@inheritdoc} + * @throws Error + * @throws JsonException */ - public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTokenEntity) + public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTokenEntity): void { if (!$accessTokenEntity instanceof AccessTokenEntity) { throw new Error('Invalid AccessTokenEntity'); @@ -73,6 +84,8 @@ public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTo /** * Find Access Token by id. + * @throws Exception + * @throws OidcServerException */ public function findById(string $tokenId): ?AccessTokenEntity { @@ -83,26 +96,29 @@ public function findById(string $tokenId): ?AccessTokenEntity ] ); - if (!$rows = $stmt->fetchAll()) { + if (empty($rows = $stmt->fetchAll())) { return null; } + /** @var array $data */ $data = current($rows); - $clientRepository = new ClientRepository($this->configurationService); - $data['client'] = $clientRepository->findById($data['client_id']); + $clientRepository = new ClientRepository($this->moduleConfig); + $data['client'] = $clientRepository->findById((string)$data['client_id']); return AccessTokenEntity::fromState($data); } /** * {@inheritdoc} + * @throws JsonException + * @throws OidcServerException */ - public function revokeAccessToken($tokenId) + public function revokeAccessToken($tokenId): void { $accessToken = $this->findById($tokenId); if (!$accessToken instanceof AccessTokenEntity) { - throw new \RuntimeException("AccessToken not found: {$tokenId}"); + throw new RuntimeException("AccessToken not found: $tokenId"); } $accessToken->revoke(); @@ -111,13 +127,14 @@ public function revokeAccessToken($tokenId) /** * {@inheritdoc} + * @throws OidcServerException */ - public function isAccessTokenRevoked($tokenId) + public function isAccessTokenRevoked($tokenId): bool { $accessToken = $this->findById($tokenId); if (!$accessToken) { - throw new \RuntimeException("AccessToken not found: {$tokenId}"); + throw new RuntimeException("AccessToken not found: $tokenId"); } return $accessToken->isRevoked(); @@ -125,6 +142,7 @@ public function isAccessTokenRevoked($tokenId) /** * Removes expired access tokens. + * @throws Exception */ public function removeExpired(): void { @@ -133,10 +151,10 @@ public function removeExpired(): void // Delete expired access tokens, but only if the corresponding refresh token is also expired. $this->database->write( - "DELETE FROM {$accessTokenTableName} WHERE expires_at < :now AND + "DELETE FROM $accessTokenTableName WHERE expires_at < :now AND NOT EXISTS ( SELECT 1 FROM {$refreshTokenTableName} - WHERE {$accessTokenTableName}.id = {$refreshTokenTableName}.access_token_id AND expires_at > :now + WHERE $accessTokenTableName.id = $refreshTokenTableName.access_token_id AND expires_at > :now )", [ 'now' => TimestampGenerator::utc()->format('Y-m-d H:i:s'), @@ -144,6 +162,9 @@ public function removeExpired(): void ); } + /** + * @throws JsonException + */ private function update(AccessTokenEntity $accessTokenEntity): void { $stmt = sprintf( diff --git a/src/Repositories/AllowedOriginRepository.php b/src/Repositories/AllowedOriginRepository.php index 214781a5..961dd513 100644 --- a/src/Repositories/AllowedOriginRepository.php +++ b/src/Repositories/AllowedOriginRepository.php @@ -1,12 +1,14 @@ fetchAll()) { + if (empty($rows = $stmt->fetchAll())) { return null; } + /** @var array $data */ $data = current($rows); - $clientRepository = new ClientRepository($this->configurationService); - $data['client'] = $clientRepository->findById($data['client_id']); + $clientRepository = new ClientRepository($this->moduleConfig); + $data['client'] = $clientRepository->findById((string)$data['client_id']); return AuthCodeEntity::fromState($data); } /** * {@inheritdoc} + * @throws JsonException + * @throws Exception */ - public function revokeAuthCode($codeId) + public function revokeAuthCode($codeId): void { $authCode = $this->findById($codeId); if (!$authCode instanceof AuthCodeEntity) { - throw new \RuntimeException("AuthCode not found: {$codeId}"); + throw new RuntimeException("AuthCode not found: $codeId"); } $authCode->revoke(); @@ -99,13 +109,14 @@ public function revokeAuthCode($codeId) /** * {@inheritdoc} + * @throws Exception */ public function isAuthCodeRevoked($codeId): bool { $authCode = $this->findById($codeId); if (!$authCode instanceof AuthCodeEntity) { - throw new \RuntimeException("AuthCode not found: {$codeId}"); + throw new RuntimeException("AuthCode not found: $codeId"); } return $authCode->isRevoked(); @@ -113,6 +124,7 @@ public function isAuthCodeRevoked($codeId): bool /** * Removes expired auth codes. + * @throws Exception */ public function removeExpired(): void { @@ -125,9 +137,9 @@ public function removeExpired(): void } /** - * @return void + * @throws JsonException */ - private function update(AuthCodeEntity $authCodeEntity) + private function update(AuthCodeEntity $authCodeEntity): void { $stmt = sprintf( <<database->applyPrefix(self::TABLE_NAME); @@ -35,6 +36,7 @@ public function getTableName(): string /** * {@inheritdoc} * @throws OAuthServerException + * @throws JsonException */ public function getClientEntity($clientIdentifier) { @@ -54,6 +56,7 @@ public function getClientEntity($clientIdentifier) /** * @inheritDoc * @throws OAuthServerException + * @throws JsonException */ public function validateClient($clientIdentifier, $clientSecret, $grantType): bool { @@ -71,26 +74,36 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType): bo } /** - * @param string $clientIdentifier - * @param ?string $owner restrict lookup to this owner - * @return ClientEntityInterface|null + * @throws OidcServerException + * @throws JsonException */ public function findById(string $clientIdentifier, ?string $owner = null): ?ClientEntityInterface { - list($query, $params) = $this->addOwnerWhereClause( + /** + * @var string $query + * @var array $params + */ + [$query, $params] = $this->addOwnerWhereClause( "SELECT * FROM {$this->getTableName()} WHERE id = :id", [ 'id' => $clientIdentifier, ], $owner ); + $stmt = $this->database->read($query, $params); - if (!$rows = $stmt->fetchAll()) { + if (empty($rows = $stmt->fetchAll())) { + return null; + } + + $row = current($rows); + + if (!is_array($row)) { return null; } - return ClientEntity::fromState(current($rows)); + return ClientEntity::fromState($row); } private function addOwnerWhereClause(string $query, array $params, ?string $owner = null): array @@ -108,10 +121,15 @@ private function addOwnerWhereClause(string $query, array $params, ?string $owne /** * @return ClientEntityInterface[] + * @throws OidcServerException|JsonException */ public function findAll(?string $owner = null): array { - list($query, $params) = $this->addOwnerWhereClause( + /** + * @var string $query + * @var array $params + */ + [$query, $params] = $this->addOwnerWhereClause( "SELECT * FROM {$this->getTableName()}", [], $owner @@ -123,6 +141,7 @@ public function findAll(?string $owner = null): array $clients = []; + /** @var array $state */ foreach ($stmt->fetchAll() as $state) { $clients[] = ClientEntity::fromState($state); } @@ -131,9 +150,6 @@ public function findAll(?string $owner = null): array } /** - * @param int $page - * @param string $query - * @param string|null $owner * @return array{numPages: int, currentPage: int, items: ClientEntityInterface[]} * @throws Exception */ @@ -145,19 +161,22 @@ public function findPaginated(int $page = 1, string $query = '', ?string $owner $numPages = $this->calculateNumOfPages($total, $limit); $page = $this->calculateCurrentPage($page, $numPages); $offset = $this->calculateOffset($page, $limit); - list($sqlQuery, $params) = $this->addOwnerWhereClause( + + /** + * @var string $sqlQuery + * @var array $params + */ + [$sqlQuery, $params] = $this->addOwnerWhereClause( "SELECT * FROM {$this->getTableName()} WHERE name LIKE :name", ['name' => '%' . $query . '%'], $owner ); $stmt = $this->database->read( - $sqlQuery . " ORDER BY name ASC LIMIT {$limit} OFFSET {$offset}", + $sqlQuery . " ORDER BY name ASC LIMIT $limit OFFSET $offset", $params ); - $clients = array_map(function ($state) { - return ClientEntity::fromState($state); - }, $stmt->fetchAll()); + $clients = array_map(fn(array $state) => ClientEntity::fromState($state), $stmt->fetchAll()); return [ 'numPages' => $numPages, @@ -210,7 +229,11 @@ public function add(ClientEntityInterface $client): void public function delete(ClientEntityInterface $client, ?string $owner = null): void { - list($sqlQuery, $params) = $this->addOwnerWhereClause( + /** + * @var string $sqlQuery + * @var array $params + */ + [$sqlQuery, $params] = $this->addOwnerWhereClause( "DELETE FROM {$this->getTableName()} WHERE id = :id", [ 'id' => $client->getIdentifier(), @@ -241,7 +264,12 @@ public function update(ClientEntityInterface $client, ?string $owner = null): vo , $this->getTableName() ); - list($sqlQuery, $params) = $this->addOwnerWhereClause( + + /** + * @var string $sqlQuery + * @var array $params + */ + [$sqlQuery, $params] = $this->addOwnerWhereClause( $stmt, $client->getState(), $owner @@ -254,7 +282,11 @@ public function update(ClientEntityInterface $client, ?string $owner = null): vo private function count(string $query, ?string $owner): int { - list($sqlQuery, $params) = $this->addOwnerWhereClause( + /** + * @var string $sqlQuery + * @var array $params + */ + [$sqlQuery, $params] = $this->addOwnerWhereClause( "SELECT COUNT(id) FROM {$this->getTableName()} WHERE name LIKE :name", ['name' => '%' . $query . '%'], $owner @@ -265,7 +297,7 @@ private function count(string $query, ?string $owner): int ); $stmt->execute(); - return (int) $stmt->fetchColumn(0); + return (int) $stmt->fetchColumn(); } /** @@ -273,26 +305,17 @@ private function count(string $query, ?string $owner): int */ private function getItemsPerPage(): int { - return $this->config->getOptionalIntegerRange('items_per_page', 1, 100, 20); + return $this->config + ->getOptionalIntegerRange(ModuleConfig::OPTION_ADMIN_UI_PAGINATION_ITEMS_PER_PAGE, 1, 100, 20); } - /** - * @param int $total - * @param int $limit - * @return int - */ private function calculateNumOfPages(int $total, int $limit): int { $numPages = (int)ceil($total / $limit); - return $numPages < 1 ? 1 : $numPages; + return max($numPages, 1); } - /** - * @param int $page - * @param int $numPages - * @return int - */ private function calculateCurrentPage(int $page, int $numPages): int { if ($page > $numPages) { @@ -306,13 +329,7 @@ private function calculateCurrentPage(int $page, int $numPages): int return $page; } - - /** - * @param int $page - * @param int $limit - * @return float|int - */ - private function calculateOffset(int $page, int $limit) + private function calculateOffset(int $page, int $limit): float|int { return ($page - 1) * $limit; } diff --git a/src/Repositories/CodeChallengeVerifiersRepository.php b/src/Repositories/CodeChallengeVerifiersRepository.php index fb759fbe..2f65be3d 100644 --- a/src/Repositories/CodeChallengeVerifiersRepository.php +++ b/src/Repositories/CodeChallengeVerifiersRepository.php @@ -1,21 +1,26 @@ codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier; } @@ -33,7 +38,6 @@ public function getAll(): array } /** - * @param string $method * @return CodeChallengeVerifierInterface|null Verifier for the method or null if not supported. */ public function get(string $method): ?CodeChallengeVerifierInterface @@ -41,10 +45,6 @@ public function get(string $method): ?CodeChallengeVerifierInterface return $this->codeChallengeVerifiers[$method] ?? null; } - /** - * @param string $method - * @return bool - */ public function has(string $method): bool { return isset($this->codeChallengeVerifiers[$method]); diff --git a/src/Repositories/Interfaces/AccessTokenRepositoryInterface.php b/src/Repositories/Interfaces/AccessTokenRepositoryInterface.php index c699dc47..76bff94e 100644 --- a/src/Repositories/Interfaces/AccessTokenRepositoryInterface.php +++ b/src/Repositories/Interfaces/AccessTokenRepositoryInterface.php @@ -1,17 +1,18 @@ + * @copyright (c) 2018 Steve Rhoades + * @license http://opensource.org/licenses/MIT MIT + */ +interface IdentityProviderInterface extends RepositoryInterface +{ + public function getUserEntityByIdentifier(string $identifier): ?UserEntityInterface; +} diff --git a/src/Repositories/Interfaces/RefreshTokenRepositoryInterface.php b/src/Repositories/Interfaces/RefreshTokenRepositoryInterface.php index 18bf488f..cf30eb48 100644 --- a/src/Repositories/Interfaces/RefreshTokenRepositoryInterface.php +++ b/src/Repositories/Interfaces/RefreshTokenRepositoryInterface.php @@ -1,22 +1,21 @@ fetchAll()) { + if (empty($rows = $stmt->fetchAll())) { return null; } + /** @var array $data */ $data = current($rows); - $accessTokenRepository = new AccessTokenRepository($this->configurationService); - $data['access_token'] = ($accessTokenRepository)->findById($data['access_token_id']); + $accessTokenRepository = new AccessTokenRepository($this->moduleConfig); + $data['access_token'] = $accessTokenRepository->findById((string)$data['access_token_id']); return RefreshTokenEntity::fromState($data); } /** * {@inheritdoc} + * @throws OidcServerException */ - public function revokeRefreshToken($tokenId) + public function revokeRefreshToken($tokenId): void { $refreshToken = $this->findById($tokenId); if (!$refreshToken) { - throw new \RuntimeException("RefreshToken not found: {$tokenId}"); + throw new RuntimeException("RefreshToken not found: $tokenId"); } $refreshToken->revoke(); @@ -106,13 +115,14 @@ public function revokeRefreshToken($tokenId) /** * {@inheritdoc} + * @throws OidcServerException */ - public function isRefreshTokenRevoked($tokenId) + public function isRefreshTokenRevoked($tokenId): bool { $refreshToken = $this->findById($tokenId); if (!$refreshToken) { - throw new \RuntimeException("RefreshToken not found: {$tokenId}"); + throw new RuntimeException("RefreshToken not found: $tokenId"); } return $refreshToken->isRevoked(); @@ -120,6 +130,7 @@ public function isRefreshTokenRevoked($tokenId) /** * Removes expired refresh tokens. + * @throws Exception */ public function removeExpired(): void { diff --git a/src/Repositories/ScopeRepository.php b/src/Repositories/ScopeRepository.php index 25c342db..b3a2defc 100644 --- a/src/Repositories/ScopeRepository.php +++ b/src/Repositories/ScopeRepository.php @@ -1,5 +1,7 @@ configurationService->getOpenIDScopes(); + $scopes = $this->moduleConfig->getOpenIDScopes(); - if (false === \array_key_exists($identifier, $scopes)) { + if (false === array_key_exists($identifier, $scopes)) { return null; } + /** @var array $scope */ $scope = $scopes[$identifier]; + /** @var ?string $description */ $description = $scope['description'] ?? null; + /** @var ?string $icon */ $icon = $scope['icon'] ?? null; - $attributes = $scope['attributes'] ?? []; + /** @var string[] $claims */ + $claims = $scope['claims'] ?? []; - $scope = ScopeEntity::fromData( + return ScopeEntity::fromData( $identifier, $description, $icon, - $attributes + $claims ); - - return $scope; } /** @@ -65,13 +69,14 @@ public function finalizeScopes( $grantType, OAuth2ClientEntityInterface $clientEntity, $userIdentifier = null - ) { + ): array { if (!$clientEntity instanceof ClientEntity) { return []; } - return array_filter($scopes, function (ScopeEntityInterface $scope) use ($clientEntity) { - return \in_array($scope->getIdentifier(), $clientEntity->getScopes(), true); - }); + return array_filter( + $scopes, + fn(ScopeEntityInterface $scope) => in_array($scope->getIdentifier(), $clientEntity->getScopes(), true) + ); } } diff --git a/src/Repositories/Traits/RevokeTokenByAuthCodeIdTrait.php b/src/Repositories/Traits/RevokeTokenByAuthCodeIdTrait.php index 96b9c51b..4e2e1aa4 100644 --- a/src/Repositories/Traits/RevokeTokenByAuthCodeIdTrait.php +++ b/src/Repositories/Traits/RevokeTokenByAuthCodeIdTrait.php @@ -1,5 +1,7 @@ database->read( "SELECT * FROM {$this->getTableName()} WHERE id = :id", @@ -42,23 +48,30 @@ public function getUserEntityByIdentifier($identifier) ] ); - if (!$rows = $stmt->fetchAll()) { + if (empty($rows = $stmt->fetchAll())) { + return null; + } + + $row = current($rows); + + if (!is_array($row)) { return null; } - return UserEntity::fromState(current($rows)); + return UserEntity::fromState($row); } /** * {@inheritdoc} + * @throws Exception */ public function getUserEntityByUserCredentials( $username, $password, $grantType, OAuth2ClientEntityInterface $clientEntity - ) { - throw new \Exception('Not supported'); + ): ?UserEntityInterface { + throw new Exception('Not supported'); } public function add(UserEntity $userEntity): void @@ -73,9 +86,6 @@ public function add(UserEntity $userEntity): void ); } - /** - * @param \SimpleSAML\Module\oidc\Entity\UserEntity $userEntity - */ public function delete(UserEntity $user): void { $this->database->write( @@ -86,9 +96,6 @@ public function delete(UserEntity $user): void ); } - /** - * @param \SimpleSAML\Module\oidc\Entity\UserEntity $userEntity - */ public function update(UserEntity $user): void { $stmt = sprintf( diff --git a/src/Server/Associations/Interfaces/RelyingPartyAssociationInterface.php b/src/Server/Associations/Interfaces/RelyingPartyAssociationInterface.php index cd02a8e7..8baf4eb1 100644 --- a/src/Server/Associations/Interfaces/RelyingPartyAssociationInterface.php +++ b/src/Server/Associations/Interfaces/RelyingPartyAssociationInterface.php @@ -1,5 +1,7 @@ clientId = $clientId; - $this->userId = $userId; - $this->sessionId = $sessionId; - $this->backChannelLogoutUri = $backChannelLogoutUri; } public function getClientId(): string diff --git a/src/Server/AuthorizationServer.php b/src/Server/AuthorizationServer.php index 3601e613..e1481ced 100644 --- a/src/Server/AuthorizationServer.php +++ b/src/Server/AuthorizationServer.php @@ -1,9 +1,13 @@ getOrFail(StateRule::class)->getValue(); + /** @var string $redirectUri */ $redirectUri = $resultBag->getOrFail(RedirectUriRule::class)->getValue(); foreach ($this->enabledGrantTypes as $grantType) { diff --git a/src/Server/Exceptions/OidcServerException.php b/src/Server/Exceptions/OidcServerException.php index 0aac337d..732a26b0 100644 --- a/src/Server/Exceptions/OidcServerException.php +++ b/src/Server/Exceptions/OidcServerException.php @@ -1,37 +1,44 @@ payload = $payload; } @@ -304,7 +311,7 @@ public function setState(string $state = null): void } /** - * Generate a HTTP response. + * Generate an HTTP response. * * @param ResponseInterface $response * @param bool $useFragment True if errors should be in the URI fragment instead of query string. Note @@ -318,6 +325,7 @@ public function generateHttpResponse( $useFragment = false, $jsonOptions = 0 ): ResponseInterface { + /** @var array $headers */ $headers = $this->getHttpHeaders(); $payload = $this->getPayload(); @@ -329,16 +337,16 @@ public function generateHttpResponse( $paramSeparator = '#'; } - $this->redirectUri .= (\strstr($this->redirectUri, $paramSeparator) === false) ? $paramSeparator : '&'; + $this->redirectUri .= (!str_contains($this->redirectUri, $paramSeparator)) ? $paramSeparator : '&'; - return $response->withStatus(302)->withHeader('Location', $this->redirectUri . \http_build_query($payload)); + return $response->withStatus(302)->withHeader('Location', $this->redirectUri . http_build_query($payload)); } foreach ($headers as $header => $content) { $response = $response->withHeader($header, $content); } - $responseBody = \json_encode($payload, $jsonOptions) ?: 'JSON encoding of payload failed'; + $responseBody = json_encode($payload, $jsonOptions) ?: 'JSON encoding of payload failed'; $response->getBody()->write($responseBody); diff --git a/src/Server/Grants/AuthCodeGrant.php b/src/Server/Grants/AuthCodeGrant.php index b8e6a185..9af558d1 100644 --- a/src/Server/Grants/AuthCodeGrant.php +++ b/src/Server/Grants/AuthCodeGrant.php @@ -1,30 +1,39 @@ setRefreshTokenRepository($refreshTokenRepository); $this->authCodeTTL = $authCodeTTL; - $this->requestRulesManager = $requestRulesManager; - $this->configurationService = $configurationService; if (in_array('sha256', hash_algos(), true)) { $s256Verifier = new S256Verifier(); @@ -109,34 +171,28 @@ public function __construct( $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier; } - /** - * @param OAuth2ClientEntityInterface $client - * @return bool - */ protected function shouldCheckPkce(OAuth2ClientEntityInterface $client): bool { - return $this->requireCodeChallengeForPublicClients && - ! $client->isConfidential(); + return $this->requireCodeChallengeForPublicClients && !$client->isConfidential(); } /** * Check if the authorization request is OIDC candidate (can respond with ID token). - * - * @param OAuth2AuthorizationRequest $authorizationRequest - * @return bool */ public function isOidcCandidate( OAuth2AuthorizationRequest $authorizationRequest ): bool { // Check if the scopes contain 'oidc' scope - return (bool) Arr::find($authorizationRequest->getScopes(), function (ScopeEntityInterface $scope) { - return $scope->getIdentifier() === 'openid'; - }); + return (bool) Arr::find( + $authorizationRequest->getScopes(), + fn(ScopeEntityInterface $scope) => $scope->getIdentifier() === 'openid' + ); } /** * @inheritDoc * @throws OAuthServerException + * @throws JsonException */ public function completeAuthorizationRequest( OAuth2AuthorizationRequest $authorizationRequest @@ -151,22 +207,21 @@ public function completeAuthorizationRequest( /** * This is reimplementation of OAuth2 completeAuthorizationRequest method with addition of nonce handling. * - * @param AuthorizationRequest $authorizationRequest - * @return RedirectResponse * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException + * @throws JsonException */ public function completeOidcAuthorizationRequest( AuthorizationRequest $authorizationRequest ): RedirectResponse { $user = $authorizationRequest->getUser(); - if ($user instanceof UserEntityInterface === false) { - throw new LogicException('An instance of UserEntityInterface should be set on the ' . + if ($user instanceof UserEntity === false) { + throw new LogicException('An instance of UserEntity should be set on the ' . 'AuthorizationRequest'); } $finalRedirectUri = $authorizationRequest->getRedirectUri() - ?? $this->getClientRedirectUri($authorizationRequest); + ?? $this->getClientRedirectUri($authorizationRequest); if ($authorizationRequest->isAuthorizationApproved() !== true) { // The user denied the client, redirect them back with an error @@ -204,12 +259,7 @@ public function completeOidcAuthorizationRequest( 'session_id' => $authorizationRequest->getSessionId(), ]; - $jsonPayload = json_encode($payload); - - if ($jsonPayload === false) { - throw new LogicException('An error was encountered when JSON encoding the authorization ' . - 'request response'); - } + $jsonPayload = json_encode($payload, JSON_THROW_ON_ERROR); $response = new RedirectResponse(); $response->setRedirectUri( @@ -226,13 +276,7 @@ public function completeOidcAuthorizationRequest( } /** - * @param DateInterval $authCodeTTL - * @param OAuth2ClientEntityInterface $client - * @param string $userIdentifier - * @param string $redirectUri - * @param array $scopes - * @param string|null $nonce - * @return AuthCodeEntityInterface + * @param ScopeEntityInterface[] $scopes * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException */ @@ -244,10 +288,18 @@ protected function issueOidcAuthCode( array $scopes = [], string $nonce = null ): AuthCodeEntityInterface { - $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; + if (! is_a($this->authCodeRepository, AuthCodeRepositoryInterface::class)) { + throw OidcServerException::serverError('Unexpected auth code repository entity type.'); + } + $authCode = $this->authCodeRepository->getNewAuthCode(); + + if (! is_a($authCode, AuthCodeEntityInterface::class)) { + throw OidcServerException::serverError('Unexpected auth code entity type.'); + } + $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL)); $authCode->setClient($client); $authCode->setUserIdentifier($userIdentifier); @@ -298,37 +350,44 @@ protected function getClientRedirectUri(OAuth2AuthorizationRequest $authorizatio * Reimplementation respondToAccessTokenRequest because of nonce feature. * * @param ServerRequestInterface $request - * @param ResponseTypeInterface $responseType - * @param DateInterval $accessTokenTTL - * - * @throws OAuthServerException + * @param ResponseTypeInterface $responseType + * @param DateInterval $accessTokenTTL * * @return ResponseTypeInterface * * TODO refactor to request checkers + * @throws OAuthServerException + * @throws JsonException + * @throws JsonException + * @throws JsonException + * */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { - list($clientId) = $this->getClientCredentials($request); + [$clientId] = $this->getClientCredentials($request); - $client = $this->getClientEntityOrFail($clientId, $request); + $client = $this->getClientEntityOrFail((string)$clientId, $request); // Only validate the client if it is confidential if ($client->isConfidential()) { $this->validateClient($request); } - $encryptedAuthCode = $this->getRequestParameter('code', $request, null); + $encryptedAuthCode = $this->getRequestParameter('code', $request); if ($encryptedAuthCode === null) { throw OAuthServerException::invalidRequest('code'); } try { - $authCodePayload = json_decode($this->decrypt($encryptedAuthCode)); + /** + * @noinspection PhpUndefinedClassInspection + * @psalm-var AuthCodePayloadObject $authCodePayload + */ + $authCodePayload = json_decode($this->decrypt($encryptedAuthCode), null, 512, JSON_THROW_ON_ERROR); $this->validateAuthorizationCode($authCodePayload, $client, $request); @@ -342,7 +401,7 @@ public function respondToAccessTokenRequest( throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e); } - $codeVerifier = $this->getRequestParameter('code_verifier', $request, null); + $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 ($this->shouldCheckPkce($client) && empty($authCodePayload->code_challenge) && $codeVerifier !== null) { @@ -383,16 +442,17 @@ public function respondToAccessTokenRequest( throw OAuthServerException::serverError( sprintf( 'Unsupported code challenge method `%s`', - $authCodePayload->code_challenge_method + ($authCodePayload->code_challenge_method ?? '') ) ); } } } + /** @var array $claims */ $claims = property_exists($authCodePayload, 'claims') ? - json_decode(json_encode($authCodePayload->claims), true) - : null; + json_decode(json_encode($authCodePayload->claims, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR) + : null; // Issue and persist new access token $accessToken = $this->issueAccessToken( @@ -426,7 +486,7 @@ public function respondToAccessTokenRequest( if ( $responseType instanceof AcrResponseTypeInterface && property_exists($authCodePayload, 'acr') && - $authCodePayload->acr !== null + ! empty($authCodePayload->acr) ) { $responseType->setAcr($authCodePayload->acr); } @@ -434,7 +494,7 @@ public function respondToAccessTokenRequest( if ( $responseType instanceof SessionIdResponseTypeInterface && property_exists($authCodePayload, 'session_id') && - $authCodePayload->session_id !== null + ! empty($authCodePayload->session_id) ) { $responseType->setSessionId($authCodePayload->session_id); } @@ -449,6 +509,9 @@ public function respondToAccessTokenRequest( $responseType->setRefreshToken($refreshToken); } } + if (! is_a($this->authCodeRepository, AuthCodeRepositoryInterface::class)) { + throw OidcServerException::serverError('Unexpected auth code repository entity type.'); + } // Revoke used auth code $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); @@ -459,20 +522,38 @@ public function respondToAccessTokenRequest( /** * Reimplementation because of private parent access * - * @param stdClass $authCodePayload + * @param object $authCodePayload * @param OAuth2ClientEntityInterface $client * @param ServerRequestInterface $request * @throws OAuthServerException + * @throws OidcServerException */ protected function validateAuthorizationCode( - stdClass $authCodePayload, + object $authCodePayload, OAuth2ClientEntityInterface $client, ServerRequestInterface $request ): void { + /** + * @noinspection PhpUndefinedClassInspection + * @psalm-var AuthCodePayloadObject $authCodePayload + */ + if (!property_exists($authCodePayload, 'auth_code_id')) { throw OAuthServerException::invalidRequest('code', 'Authorization code malformed'); } + if (! is_a($this->authCodeRepository, AuthCodeRepositoryInterface::class)) { + throw OidcServerException::serverError('Unexpected auth code repository entity type.'); + } + + if (! is_a($this->accessTokenRepository, AccessTokenRepositoryInterface::class)) { + throw OidcServerException::serverError('Unexpected access token repository entity type.'); + } + + if (! is_a($this->refreshTokenRepository, RefreshTokenRepositoryInterface::class)) { + throw OidcServerException::serverError('Unexpected refresh token repository entity type.'); + } + if (time() > $authCodePayload->expire_time) { throw OAuthServerException::invalidGrant('Authorization code has expired'); } @@ -489,13 +570,16 @@ protected function validateAuthorizationCode( } // The redirect URI is required in this request - $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); + $redirectUri = $this->getRequestParameter('redirect_uri', $request); if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) { throw OAuthServerException::invalidRequest('redirect_uri'); } if ($authCodePayload->redirect_uri !== $redirectUri) { - throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); + throw OAuthServerException::invalidRequest( + 'redirect_uri', + 'Invalid redirect URI or not the same as in authorization request' + ); } } @@ -517,7 +601,7 @@ public function validateAuthorizationRequestWithCheckerResultBag( ScopeOfflineAccessRule::class, ]; - // Since we have already validated redirect_uri and we have state, make it available for other checkers. + // Since we have already validated redirect_uri, and we have state, make it available for other checkers. $this->requestRulesManager->predefineResultBag($resultBag); /** @var string $redirectUri */ @@ -539,7 +623,7 @@ public function validateAuthorizationRequestWithCheckerResultBag( $resultBag = $this->requestRulesManager->check($request, $rulesToExecute); - /** @var array $scopes */ + /** @var ScopeEntityInterface[] $scopes */ $scopes = $resultBag->getOrFail(ScopeRule::class)->getValue(); $oAuth2AuthorizationRequest = new OAuth2AuthorizationRequest(); @@ -556,6 +640,7 @@ public function validateAuthorizationRequestWithCheckerResultBag( if ($shouldCheckPkce) { /** @var string $codeChallenge */ $codeChallenge = $resultBag->getOrFail(CodeChallengeRule::class)->getValue(); + /** @var string $codeChallengeMethod */ $codeChallengeMethod = $resultBag->getOrFail(CodeChallengeMethodRule::class)->getValue(); $oAuth2AuthorizationRequest->setCodeChallenge($codeChallenge); @@ -581,7 +666,11 @@ public function validateAuthorizationRequestWithCheckerResultBag( $requestClaims = $resultBag->get(RequestedClaimsRule::class); if (null !== $requestClaims) { - $authorizationRequest->setClaims($requestClaims->getValue()); + /** @var ?array $requestClaimValues */ + $requestClaimValues = $requestClaims->getValue(); + if (is_array($requestClaimValues)) { + $authorizationRequest->setClaims($requestClaimValues); + } } /** @var array|null $acrValues */ @@ -602,6 +691,10 @@ protected function issueRefreshToken( OAuth2AccessTokenEntityInterface $accessToken, string $authCodeId = null ): ?RefreshTokenEntityInterface { + if (! is_a($this->refreshTokenRepository, RefreshTokenRepositoryInterface::class)) { + throw OidcServerException::serverError('Unexpected refresh token repository entity type.'); + } + $refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); if ($refreshToken === null) { diff --git a/src/Server/Grants/ImplicitGrant.php b/src/Server/Grants/ImplicitGrant.php index 3b39e17d..f19f6a24 100644 --- a/src/Server/Grants/ImplicitGrant.php +++ b/src/Server/Grants/ImplicitGrant.php @@ -1,9 +1,12 @@ accessTokenRepository = $accessTokenRepository; - $this->idTokenBuilder = $idTokenBuilder; } /** @@ -56,14 +63,18 @@ public function __construct( public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool { $queryParams = $request->getQueryParams(); - if (!isset($queryParams['response_type']) || !isset($queryParams['client_id'])) { + if ( + !isset($queryParams['response_type']) || + !is_string($queryParams['response_type']) || + !isset($queryParams['client_id']) + ) { return false; } $responseType = explode(" ", $queryParams['response_type']); return in_array('id_token', $responseType, true) && - ! in_array('code', $responseType, true); // ...avoid triggering hybrid flow + ! in_array('code', $responseType, true); // ...avoid triggering hybrid flow } /** @@ -93,7 +104,7 @@ public function validateAuthorizationRequestWithCheckerResultBag( ResultBagInterface $resultBag ): OAuth2AuthorizationRequest { $oAuth2AuthorizationRequest = - parent::validateAuthorizationRequestWithCheckerResultBag($request, $resultBag); + parent::validateAuthorizationRequestWithCheckerResultBag($request, $resultBag); $rulesToExecute = [ RequestParameterRule::class, @@ -114,7 +125,7 @@ public function validateAuthorizationRequestWithCheckerResultBag( $authorizationRequest = AuthorizationRequest::fromOAuth2AuthorizationRequest($oAuth2AuthorizationRequest); // nonce existence is validated using a rule, so we can get it from there. - $authorizationRequest->setNonce($resultBag->getOrFail(RequiredNonceRule::class)->getValue()); + $authorizationRequest->setNonce((string)$resultBag->getOrFail(RequiredNonceRule::class)->getValue()); $maxAge = $resultBag->get(MaxAgeRule::class); if (null !== $maxAge) { @@ -123,8 +134,13 @@ public function validateAuthorizationRequestWithCheckerResultBag( $requestClaims = $resultBag->get(RequestedClaimsRule::class); if (null !== $requestClaims) { - $authorizationRequest->setClaims($requestClaims->getValue()); + /** @var ?array $requestClaimValues */ + $requestClaimValues = $requestClaims->getValue(); + if (is_array($requestClaimValues)) { + $authorizationRequest->setClaims($requestClaimValues); + } } + /** @var bool $addClaimsToIdToken */ $addClaimsToIdToken = ($resultBag->getOrFail(AddClaimsToIdTokenRule::class))->getValue(); $authorizationRequest->setAddClaimsToIdToken($addClaimsToIdToken); @@ -143,12 +159,13 @@ public function validateAuthorizationRequestWithCheckerResultBag( * @throws UniqueTokenIdentifierConstraintViolationException * @throws OAuthServerException * @throws OidcServerException + * @throws Exception */ private function completeOidcAuthorizationRequest(AuthorizationRequest $authorizationRequest): ResponseTypeInterface { $user = $authorizationRequest->getUser(); - if ($user instanceof UserEntityInterface === false) { + if ($user instanceof UserEntity === false) { throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } diff --git a/src/Server/Grants/Interfaces/AuthorizationValidatableWithCheckerResultBagInterface.php b/src/Server/Grants/Interfaces/AuthorizationValidatableWithCheckerResultBagInterface.php index e0794c1f..4a3d5259 100644 --- a/src/Server/Grants/Interfaces/AuthorizationValidatableWithCheckerResultBagInterface.php +++ b/src/Server/Grants/Interfaces/AuthorizationValidatableWithCheckerResultBagInterface.php @@ -1,5 +1,7 @@ requestRulesManager->predefineResultBag($resultBag); /** @var string $redirectUri */ @@ -73,7 +135,7 @@ public function validateAuthorizationRequestWithCheckerResultBag( $resultBag = $this->requestRulesManager->check($request, $rulesToExecute); - /** @var array $scopes */ + /** @var ScopeEntityInterface[] $scopes */ $scopes = $resultBag->getOrFail(ScopeRule::class)->getValue(); $oAuth2AuthorizationRequest = new OAuth2AuthorizationRequest(); diff --git a/src/Server/Grants/RefreshTokenGrant.php b/src/Server/Grants/RefreshTokenGrant.php index 568f1afc..686a9194 100644 --- a/src/Server/Grants/RefreshTokenGrant.php +++ b/src/Server/Grants/RefreshTokenGrant.php @@ -1,19 +1,77 @@ getRequestParameter('refresh_token', $request); - if (\is_null($encryptedRefreshToken)) { + if (is_null($encryptedRefreshToken)) { throw OidcServerException::invalidGrant('Failed to verify `refresh_token`'); } @@ -24,18 +82,26 @@ protected function validateOldRefreshToken(ServerRequestInterface $request, $cli throw OidcServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); } - $refreshTokenData = \json_decode($refreshToken, true); + $refreshTokenData = json_decode($refreshToken, true, 512, JSON_THROW_ON_ERROR); + + if (! is_array($refreshTokenData)) { + throw OidcServerException::invalidRefreshToken('Refresh token has unexpected type'); + } if ($refreshTokenData['client_id'] !== $clientId) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); - throw OidcServerException::invalidRefreshToken('Token is not linked to client'); + throw OidcServerException::invalidRefreshToken('Refresh token is not linked to client'); } - if ($refreshTokenData['expire_time'] < \time()) { - throw OidcServerException::invalidRefreshToken('Token has expired'); + if ($refreshTokenData['expire_time'] < time()) { + throw OidcServerException::invalidRefreshToken('Refresh token has expired'); } - if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { - throw OidcServerException::invalidRefreshToken('Token has been revoked'); + if ( + $this->refreshTokenRepository->isRefreshTokenRevoked( + (string)$refreshTokenData['refresh_token_id'] + ) === true + ) { + throw OidcServerException::invalidRefreshToken('Refresh token has been revoked'); } return $refreshTokenData; diff --git a/src/Server/Grants/Traits/IssueAccessTokenTrait.php b/src/Server/Grants/Traits/IssueAccessTokenTrait.php index 3f057be1..2a4f11b9 100644 --- a/src/Server/Grants/Traits/IssueAccessTokenTrait.php +++ b/src/Server/Grants/Traits/IssueAccessTokenTrait.php @@ -1,5 +1,7 @@ accessTokenRepository, AccessTokenRepositoryInterface::class)) { + throw OidcServerException::serverError( + 'Access token repository does not implement ' . AccessTokenRepositoryInterface::class + ); + } + $accessToken = $this->accessTokenRepository->getNewToken( $client, $scopes, @@ -81,7 +89,6 @@ protected function issueAccessToken( * Generate a new unique identifier. * * @param int $length - * * @throws OAuthServerException * * @return string diff --git a/src/Server/LogoutHandlers/BackChannelLogoutHandler.php b/src/Server/LogoutHandlers/BackChannelLogoutHandler.php index d3989312..1b0dda42 100644 --- a/src/Server/LogoutHandlers/BackChannelLogoutHandler.php +++ b/src/Server/LogoutHandlers/BackChannelLogoutHandler.php @@ -1,5 +1,7 @@ logoutTokenBuilder = $logoutTokenBuilder ?? new LogoutTokenBuilder(); - $this->loggerService = $loggerService ?? new LoggerService(); } /** - * @param array $relyingPartyAssociations + * @param array $relyingPartyAssociations * @param HandlerStack|null $handlerStack For easier testing * @throws OAuthServerException */ @@ -42,16 +38,16 @@ public function handle(array $relyingPartyAssociations, HandlerStack $handlerSta $pool = new Pool($client, $this->logoutRequestsGenerator($relyingPartyAssociations), [ 'concurrency' => 5, - 'fulfilled' => function (Response $response, $index) { + 'fulfilled' => function (Response $response, mixed $index) { // this is delivered each successful response $successMessage = "Backhannel Logout (index $index) - success, status: {$response->getStatusCode()} " . "{$response->getReasonPhrase()}"; $this->loggerService->notice($successMessage); }, - 'rejected' => function (GuzzleException $reason, $index) { + 'rejected' => function (GuzzleException $reason, mixed $index) { // this is delivered each failed request $errorMessage = "Backhannel Logout (index $index) - error, reason: {$reason->getCode()} " . - "{$reason->getMessage()}, exception type: " . get_class($reason); + "{$reason->getMessage()}, exception type: " . $reason::class; $this->loggerService->error($errorMessage); }, ]); @@ -64,7 +60,7 @@ public function handle(array $relyingPartyAssociations, HandlerStack $handlerSta } /** - * @param array $relyingPartyAssociations + * @param array $relyingPartyAssociations * @return Generator * @throws OAuthServerException */ @@ -74,7 +70,7 @@ protected function logoutRequestsGenerator(array $relyingPartyAssociations): Gen foreach ($relyingPartyAssociations as $association) { if ($association->getBackChannelLogoutUri() !== null) { $logMessage = "Backhannel Logout (index $index) - preparing request to: " . - $association->getBackChannelLogoutUri(); + ($association->getBackChannelLogoutUri() ?? ''); $this->loggerService->notice($logMessage); $index++; diff --git a/src/Server/RequestTypes/AuthorizationRequest.php b/src/Server/RequestTypes/AuthorizationRequest.php index 7c5f5a38..0b99bc3a 100644 --- a/src/Server/RequestTypes/AuthorizationRequest.php +++ b/src/Server/RequestTypes/AuthorizationRequest.php @@ -1,5 +1,7 @@ nonce; } - /** - * @param string $nonce - */ public function setNonce(string $nonce): void { $this->nonce = $nonce; @@ -119,17 +118,11 @@ public function getAddClaimsToIdToken(): bool return $this->addClaimsToIdToken; } - /** - * @param bool $addClaimsToIdToken - */ public function setAddClaimsToIdToken(bool $addClaimsToIdToken): void { $this->addClaimsToIdToken = $addClaimsToIdToken; } - /** - * @param string $responseType - */ public function setResponseType(string $responseType): void { $this->responseType = $responseType; diff --git a/src/Server/RequestTypes/LogoutRequest.php b/src/Server/RequestTypes/LogoutRequest.php index 12e4db0b..c3170497 100644 --- a/src/Server/RequestTypes/LogoutRequest.php +++ b/src/Server/RequestTypes/LogoutRequest.php @@ -1,48 +1,38 @@ idTokenHint = $idTokenHint; - $this->postLogoutRedirectUri = $postLogoutRedirectUri; - $this->state = $state; - $this->uiLocales = $uiLocales; } public function getIdTokenHint(): ?UnencryptedToken diff --git a/src/Server/ResponseTypes/IdTokenResponse.php b/src/Server/ResponseTypes/IdTokenResponse.php index e98ab5d7..e6aa60c2 100644 --- a/src/Server/ResponseTypes/IdTokenResponse.php +++ b/src/Server/ResponseTypes/IdTokenResponse.php @@ -1,5 +1,7 @@ identityProvider = $identityProvider; - $this->idTokenBuilder = $idTokenBuilder; - $this->configurationService = $configurationService; + $this->privateKey = $privateKey; } /** @@ -82,8 +93,17 @@ protected function getExtraParams(AccessTokenEntityInterface $accessToken): arra throw new RuntimeException('AccessToken must be ' . AccessTokenEntity::class); } - /** @var UserEntityInterface $userEntity */ - $userEntity = $this->identityProvider->getUserEntityByIdentifier($accessToken->getUserIdentifier()); + $userIdentifier = $accessToken->getUserIdentifier(); + + if (empty($userIdentifier)) { + throw OidcServerException::accessDenied('No user identifier present in AccessToken.'); + } + + $userEntity = $this->identityProvider->getUserEntityByIdentifier((string)$userIdentifier); + + if (empty($userEntity)) { + throw OidcServerException::accessDenied('No user available for provided user identifier.'); + } $token = $this->idTokenBuilder->build( $userEntity, diff --git a/src/Server/ResponseTypes/Interfaces/AcrResponseTypeInterface.php b/src/Server/ResponseTypes/Interfaces/AcrResponseTypeInterface.php index 8e010c1e..98cddc65 100644 --- a/src/Server/ResponseTypes/Interfaces/AcrResponseTypeInterface.php +++ b/src/Server/ResponseTypes/Interfaces/AcrResponseTypeInterface.php @@ -1,5 +1,7 @@ accessTokenRepository = $accessTokenRepository; + $this->setPublicKey($publicKey); } /** * Set the public key * * @param CryptKey $key + * @throws Exception */ - public function setPublicKey(CryptKey $key) + public function setPublicKey(CryptKey $key): void { $this->publicKey = $key; @@ -58,17 +76,18 @@ public function setPublicKey(CryptKey $key) /** * Initialise the JWT configuration. + * @throws Exception */ - protected function initJwtConfiguration() + protected function initJwtConfiguration(): void { $this->jwtConfiguration = Configuration::forSymmetricSigner( new Sha256(), - InMemory::empty() + InMemory::plainText('empty', 'empty') ); /** @psalm-suppress ArgumentTypeCoercion */ $this->jwtConfiguration->setValidationConstraints( - new StrictValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))), + new StrictValidAt(new SystemClock(new DateTimeZone(date_default_timezone_get()))), new SignedWith( new Sha256(), InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '') @@ -86,16 +105,17 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe if ($request->hasHeader('authorization')) { $header = $request->getHeader('authorization'); - $jwt = \trim((string) \preg_replace('/^\s*Bearer\s/', '', $header[0])); + $jwt = trim((string) preg_replace('/^\s*Bearer\s/', '', $header[0])); } elseif ( strcasecmp($request->getMethod(), 'POST') === 0 && is_array($parsedBody = $request->getParsedBody()) && - isset($parsedBody['access_token']) + isset($parsedBody['access_token']) && + is_string($parsedBody['access_token']) ) { $jwt = $parsedBody['access_token']; } - if ($jwt === null) { + if (!is_string($jwt)) { throw OidcServerException::accessDenied('Missing Authorization header or access_token request body param.'); } @@ -111,14 +131,14 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe // Attempt to validate the JWT $constraints = $this->jwtConfiguration->validationConstraints(); $this->jwtConfiguration->validator()->assert($token, ...$constraints); - } catch (RequiredConstraintsViolated $exception) { + } catch (RequiredConstraintsViolated) { throw OidcServerException::accessDenied('Access token could not be verified'); } $claims = $token->claims(); - if (is_null($jti = $claims->get('jti')) || empty($jti)) { - throw OidcServerException::accessDenied('Access token malformed (jti missing)'); + if (is_null($jti = $claims->get('jti')) || empty($jti) || !is_string($jti)) { + throw OidcServerException::accessDenied('Access token malformed (jti missing or unexpected type)'); } // Check if token has been revoked @@ -140,9 +160,22 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe * @param mixed $aud * * @return array|string + * @throws OidcServerException */ - protected function convertSingleRecordAudToString($aud) + protected function convertSingleRecordAudToString(mixed $aud): array|string { - return \is_array($aud) && \count($aud) === 1 ? $aud[0] : $aud; + if (is_string($aud)) { + return $aud; + } + + if (is_array($aud) && !empty($aud)) { + if (count($aud) === 1) { + return (string)$aud[0]; + } else { + return $aud; + } + } + + throw OidcServerException::accessDenied('Unexpected sub claim value.'); } } diff --git a/src/Services/AuthContextService.php b/src/Services/AuthContextService.php index e2a9e74a..b230b2a5 100644 --- a/src/Services/AuthContextService.php +++ b/src/Services/AuthContextService.php @@ -1,8 +1,13 @@ configurationService = $configurationService; - $this->authSimpleFactory = $authSimpleFactory; + public function __construct( + private readonly ModuleConfig $moduleConfig, + private readonly AuthSimpleFactory $authSimpleFactory + ) { } public function isSspAdmin(): bool { + // TODO mivanci make bridge to SSP utility classes (search for SSP namespace through the codebase) return (new Auth())->isAdmin(); } + /** + * @throws Exception + * @throws \Exception + */ public function getAuthUserId(): string { $simple = $this->authenticate(); - $userIdAttr = $this->configurationService->getOpenIDConnectConfiguration()->getString('useridattr'); - return (new Attributes())->getExpectedAttribute($simple->getAttributes(), $userIdAttr); + $userIdAttr = $this->moduleConfig->getUserIdentifierAttribute(); + return (string)(new Attributes())->getExpectedAttribute($simple->getAttributes(), $userIdAttr); } /** @@ -56,21 +54,22 @@ public function getAuthUserId(): string * @param string $neededPermission The permissions needed * @throws \Exception thrown if permissions are not enabled or user is missing the needed entitlements */ - public function requirePermission(string $neededPermission) + public function requirePermission(string $neededPermission): void { $auth = $this->authenticate(); - $permissions = $this->configurationService - ->getOpenIDConnectConfiguration() - ->getOptionalConfigItem('permissions', null); - /** @psalm-suppress DocblockTypeContradiction */ + $permissions = $this->moduleConfig + ->config() + ->getOptionalConfigItem(ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS, null); + if (is_null($permissions) || !$permissions->hasValue('attribute')) { - throw new \RuntimeException('Permissions not enabled'); + throw new RuntimeException('Permissions not enabled'); } if (!$permissions->hasValue($neededPermission)) { - throw new \RuntimeException('No permission defined for ' . $neededPermission); + throw new RuntimeException('No permission defined for ' . $neededPermission); } $attributeName = $permissions->getString('attribute'); + /** @var string[] $entitlements */ $entitlements = $auth->getAttributes()[$attributeName] ?? []; $neededEntitlements = $permissions->getArrayizeString($neededPermission); foreach ($entitlements as $entitlement) { @@ -78,9 +77,12 @@ public function requirePermission(string $neededPermission) return; } } - throw new \RuntimeException('Missing entitlement for ' . $neededPermission); + throw new RuntimeException('Missing entitlement for ' . $neededPermission); } + /** + * @throws \Exception + */ private function authenticate(): Simple { $simple = $this->authSimpleFactory->getDefaultAuthSource(); diff --git a/src/Services/AuthProcService.php b/src/Services/AuthProcService.php index d7930ee9..a9e68cb8 100644 --- a/src/Services/AuthProcService.php +++ b/src/Services/AuthProcService.php @@ -1,43 +1,40 @@ configurationService = $configurationService; $this->loadFilters(); } /** * Load filters defined in configuration. - * @throws \Exception + * @throws Exception */ private function loadFilters(): void { - $oidcAuthProcFilters = $this->configurationService->getAuthProcFilters(); + $oidcAuthProcFilters = $this->moduleConfig->getAuthProcFilters(); $this->filters = $this->parseFilterList($oidcAuthProcFilters); } @@ -46,8 +43,8 @@ private function loadFilters(): void * @see \SimpleSAML\Auth\ProcessingChain::parseFilterList for original implementation * * @param array $filterSrc Array with filter configuration. - * @return array Array of ProcessingFilter objects. - * @throws \Exception + * @return array Array of ProcessingFilter objects. + * @throws Exception */ private function parseFilterList(array $filterSrc): array { @@ -59,25 +56,35 @@ private function parseFilterList(array $filterSrc): array } if (!is_array($filterConfig)) { - throw new \Exception('Invalid authentication processing filter configuration: ' . + throw new Exception('Invalid authentication processing filter configuration: ' . 'One of the filters wasn\'t a string or an array.'); } if (!array_key_exists('class', $filterConfig)) { - throw new \Exception('Authentication processing filter without name given.'); + throw new Exception('Authentication processing filter without name given.'); + } + + if (!is_string($filterConfig['class'])) { + throw new Exception('Invalid class value for authentication processing filter configuration.'); } $className = Module::resolveClass( $filterConfig['class'], 'Auth\Process', - '\SimpleSAML\Auth\ProcessingFilter' + '\\' . ProcessingFilter::class ); + if (!is_a($className, ProcessingFilter::class, true)) { + throw new Exception( + 'Authentication processing filter class configuration is not ProcessingFilter instance.' + ); + } + $filterConfig['%priority'] = $priority; unset($filterConfig['class']); /** - * @psalm-suppress InvalidStringClass + * @psalm-suppress UnsafeInstantiation */ $parsedFilters[] = new $className($filterConfig, null); } @@ -87,9 +94,6 @@ private function parseFilterList(array $filterSrc): array /** * Process given state array. - * - * @param array $state - * @return array */ public function processState(array $state): array { diff --git a/src/Services/AuthenticationService.php b/src/Services/AuthenticationService.php index 93bd484f..1fcb006b 100644 --- a/src/Services/AuthenticationService.php +++ b/src/Services/AuthenticationService.php @@ -1,5 +1,7 @@ userRepository = $userRepository; - $this->authSimpleFactory = $authSimpleFactory; - $this->authProcService = $authProcService; $this->clientRepository = $clientRepository; - $this->oidcOpenIdProviderMetadataService = $oidcOpenIdProviderMetadataService; - $this->sessionService = $sessionService; - $this->claimTranslatorExtractor = $claimTranslatorExtractor; - $this->userIdAttr = $userIdAttr; + $this->userIdAttr = $moduleConfig->getUserIdentifierAttribute(); } /** - * @param ServerRequestInterface $request - * @param array $loginParams - * @param bool $forceAuthn - * @return UserEntity * @throws Error\Exception * @throws Error\AuthSource * @throws Error\BadRequest @@ -112,16 +95,21 @@ public function getAuthenticateUser( $state = $this->prepareStateArray($authSimple, $oidcClient, $request); $state = $this->authProcService->processState($state); + + if (!isset($state['Attributes']) || !is_array($state['Attributes'])) { + throw new Error\Exception('State array does not contain any attributes.'); + } + $claims = $state['Attributes']; - if (!array_key_exists($this->userIdAttr, $claims)) { + if (!array_key_exists($this->userIdAttr, $claims) || !is_array($claims[$this->userIdAttr])) { $attr = implode(', ', array_keys($claims)); throw new Error\Exception( 'Attribute `useridattr` doesn\'t exists in claims. Available attributes are: ' . $attr ); } - $userId = $claims[$this->userIdAttr][0]; + $userId = (string)$claims[$this->userIdAttr][0]; $user = $this->userRepository->getUserEntityByIdentifier($userId); if (!$user) { @@ -137,12 +125,6 @@ public function getAuthenticateUser( return $user; } - /** - * @param Simple $authSimple - * @param ClientEntityInterface $client - * @param ServerRequestInterface $request - * @return array - */ private function prepareStateArray( Simple $authSimple, ClientEntityInterface $client, @@ -151,17 +133,23 @@ private function prepareStateArray( $state = $authSimple->getAuthDataArray(); $state['Oidc'] = [ - 'OpenIdProviderMetadata' => $this->oidcOpenIdProviderMetadataService->getMetadata(), - 'RelyingPartyMetadata' => array_filter($client->toArray(), function (string $key) { - return $key !== 'secret'; - }, ARRAY_FILTER_USE_KEY), - 'AuthorizationRequestParameters' => array_filter($request->getQueryParams(), function (string $key) { - $relevantAuthzParams = ['response_type', 'client_id', 'redirect_uri', 'scope', 'code_challenge_method']; - return in_array($key, $relevantAuthzParams); - }, ARRAY_FILTER_USE_KEY), + 'OpenIdProviderMetadata' => $this->opMetadataService->getMetadata(), + 'RelyingPartyMetadata' => array_filter( + $client->toArray(), + fn(/** @param array-key $key */ $key) => $key !== 'secret', + ARRAY_FILTER_USE_KEY + ), + 'AuthorizationRequestParameters' => array_filter( + $request->getQueryParams(), + function (/** @param array-key $key */ $key) { + $authzParams = ['response_type', 'client_id', 'redirect_uri', 'scope', 'code_challenge_method']; + return in_array($key, $authzParams); + }, + ARRAY_FILTER_USE_KEY + ), ]; - // Source and destination entity IDs, useful for eg. F-ticks logging... + // Source and destination entity IDs, useful for e.g. F-ticks logging... $state['Source'] = ['entityid' => $state['Oidc']['OpenIdProviderMetadata']['issuer']]; $state['Destination'] = ['entityid' => $state['Oidc']['RelyingPartyMetadata']['id']]; @@ -187,8 +175,6 @@ public function getSessionId(): ?string /** * Store Relying Party Association to the current session. - * @param ClientEntityInterface $oidcClient - * @param UserEntity $user * @throws Exception */ protected function addRelyingPartyAssociation(ClientEntityInterface $oidcClient, UserEntity $user): void @@ -199,7 +185,7 @@ protected function addRelyingPartyAssociation(ClientEntityInterface $oidcClient, $this->sessionService->addRelyingPartyAssociation( new RelyingPartyAssociation( $oidcClient->getIdentifier(), - $claims['sub'] ?? $user->getIdentifier(), + (string)($claims['sub'] ?? $user->getIdentifier()), $this->getSessionId(), $oidcClient->getBackChannelLogoutUri() ) diff --git a/src/Services/Container.php b/src/Services/Container.php index 7e2fccaf..215642a0 100644 --- a/src/Services/Container.php +++ b/src/Services/Container.php @@ -1,5 +1,7 @@ services[ConfigurationService::class] = $configurationService; + $moduleConfig = new ModuleConfig(); + $this->services[ModuleConfig::class] = $moduleConfig; $loggerService = new LoggerService(); $this->services[LoggerService::class] = $loggerService; - $clientRepository = new ClientRepository($configurationService); + $clientRepository = new ClientRepository($moduleConfig); $this->services[ClientRepository::class] = $clientRepository; - $userRepository = new UserRepository($configurationService); + $userRepository = new UserRepository($moduleConfig); $this->services[UserRepository::class] = $userRepository; - $authCodeRepository = new AuthCodeRepository($configurationService); + $authCodeRepository = new AuthCodeRepository($moduleConfig); $this->services[AuthCodeRepository::class] = $authCodeRepository; - $refreshTokenRepository = new RefreshTokenRepository($configurationService); + $refreshTokenRepository = new RefreshTokenRepository($moduleConfig); $this->services[RefreshTokenRepository::class] = $refreshTokenRepository; - $accessTokenRepository = new AccessTokenRepository($configurationService); + $accessTokenRepository = new AccessTokenRepository($moduleConfig); $this->services[AccessTokenRepository::class] = $accessTokenRepository; - $scopeRepository = new ScopeRepository($configurationService); + $scopeRepository = new ScopeRepository($moduleConfig); $this->services[ScopeRepository::class] = $scopeRepository; - $allowedOriginRepository = new AllowedOriginRepository($configurationService); + $allowedOriginRepository = new AllowedOriginRepository($moduleConfig); $this->services[AllowedOriginRepository::class] = $allowedOriginRepository; $database = Database::getInstance(); @@ -123,16 +125,16 @@ public function __construct() $databaseLegacyOAuth2Import = new DatabaseLegacyOAuth2Import($clientRepository); $this->services[DatabaseLegacyOAuth2Import::class] = $databaseLegacyOAuth2Import; - $authSimpleFactory = new AuthSimpleFactory($clientRepository, $configurationService); + $authSimpleFactory = new AuthSimpleFactory($clientRepository, $moduleConfig); $this->services[AuthSimpleFactory::class] = $authSimpleFactory; - $authContextService = new AuthContextService($configurationService, $authSimpleFactory); + $authContextService = new AuthContextService($moduleConfig, $authSimpleFactory); $this->services[AuthContextService::class] = $authContextService; - $formFactory = new FormFactory($configurationService); + $formFactory = new FormFactory($moduleConfig); $this->services[FormFactory::class] = $formFactory; - $jsonWebKeySetService = new JsonWebKeySetService($configurationService); + $jsonWebKeySetService = new JsonWebKeySetService($moduleConfig); $this->services[JsonWebKeySetService::class] = $jsonWebKeySetService; $session = Session::getSessionFromRequest(); @@ -147,17 +149,17 @@ public function __construct() $templateFactory = new TemplateFactory($simpleSAMLConfiguration); $this->services[TemplateFactory::class] = $templateFactory; - $authProcService = new AuthProcService($configurationService); + $authProcService = new AuthProcService($moduleConfig); $this->services[AuthProcService::class] = $authProcService; - $oidcOpenIdProviderMetadataService = new OidcOpenIdProviderMetadataService($configurationService); - $this->services[OidcOpenIdProviderMetadataService::class] = $oidcOpenIdProviderMetadataService; + $opMetadataService = new OpMetadataService($moduleConfig); + $this->services[OpMetadataService::class] = $opMetadataService; $metadataStorageHandler = MetaDataStorageHandler::getMetadataHandler(); $this->services[MetaDataStorageHandler::class] = $metadataStorageHandler; $claimTranslatorExtractor = (new ClaimTranslatorExtractorFactory( - $configurationService + $moduleConfig ))->build(); $this->services[ClaimTranslatorExtractor::class] = $claimTranslatorExtractor; @@ -166,25 +168,17 @@ public function __construct() $authSimpleFactory, $authProcService, $clientRepository, - $oidcOpenIdProviderMetadataService, + $opMetadataService, $sessionService, $claimTranslatorExtractor, - $oidcModuleConfiguration->getOptionalString('useridattr', 'uid') + $moduleConfig, ); $this->services[AuthenticationService::class] = $authenticationService; $codeChallengeVerifiersRepository = new CodeChallengeVerifiersRepository(); $this->services[CodeChallengeVerifiersRepository::class] = $codeChallengeVerifiersRepository; - $publicKeyPath = $configurationService->getCertPath(); - $privateKeyPath = $configurationService->getPrivateKeyPath(); - $passPhrase = $configurationService->getPrivateKeyPassPhrase(); - - $cryptKeyFactory = new CryptKeyFactory( - $publicKeyPath, - $privateKeyPath, - $passPhrase - ); + $cryptKeyFactory = new CryptKeyFactory($moduleConfig); $requestRules = [ new StateRule(), @@ -201,29 +195,29 @@ public function __construct() new AddClaimsToIdTokenRule(), new RequiredNonceRule(), new ResponseTypeRule(), - new IdTokenHintRule($configurationService, $cryptKeyFactory), + new IdTokenHintRule($moduleConfig, $cryptKeyFactory), new PostLogoutRedirectUriRule($clientRepository), new UiLocalesRule(), new AcrValuesRule(), - new ScopeOfflineAccessRule($configurationService), + new ScopeOfflineAccessRule(), ]; $requestRuleManager = new RequestRulesManager($requestRules, $loggerService); $this->services[RequestRulesManager::class] = $requestRuleManager; $accessTokenDuration = new DateInterval( - $configurationService->getOpenIDConnectConfiguration()->getString('accessTokenDuration') + $moduleConfig->config()->getString(ModuleConfig::OPTION_TOKEN_ACCESS_TOKEN_TTL) ); $authCodeDuration = new DateInterval( - $configurationService->getOpenIDConnectConfiguration()->getString('authCodeDuration') + $moduleConfig->config()->getString(ModuleConfig::OPTION_TOKEN_AUTHORIZATION_CODE_TTL) ); $refreshTokenDuration = new DateInterval( - $configurationService->getOpenIDConnectConfiguration()->getString('refreshTokenDuration') + $moduleConfig->config()->getString(ModuleConfig::OPTION_TOKEN_REFRESH_TOKEN_TTL) ); $publicKey = $cryptKeyFactory->buildPublicKey(); $privateKey = $cryptKeyFactory->buildPrivateKey(); $encryptionKey = (new Config())->getSecretSalt(); - $jsonWebTokenBuilderService = new JsonWebTokenBuilderService($configurationService); + $jsonWebTokenBuilderService = new JsonWebTokenBuilderService($moduleConfig); $this->services[JsonWebTokenBuilderService::class] = $jsonWebTokenBuilderService; $idTokenBuilder = new IdTokenBuilder($jsonWebTokenBuilderService, $claimTranslatorExtractor); @@ -232,15 +226,14 @@ public function __construct() $logoutTokenBuilder = new LogoutTokenBuilder($jsonWebTokenBuilderService); $this->services[LogoutTokenBuilder::class] = $logoutTokenBuilder; - $sessionLogoutTicketStoreDb = new SessionLogoutTicketStoreDb($database); - $this->services[SessionLogoutTicketStoreDb::class] = $sessionLogoutTicketStoreDb; + $sessionLogoutTicketStoreDb = new LogoutTicketStoreDb($database); + $this->services[LogoutTicketStoreDb::class] = $sessionLogoutTicketStoreDb; - $sessionLogoutTicketStoreBuilder = new SessionLogoutTicketStoreBuilder($sessionLogoutTicketStoreDb); - $this->services[SessionLogoutTicketStoreBuilder::class] = $sessionLogoutTicketStoreBuilder; + $sessionLogoutTicketStoreBuilder = new LogoutTicketStoreBuilder($sessionLogoutTicketStoreDb); + $this->services[LogoutTicketStoreBuilder::class] = $sessionLogoutTicketStoreBuilder; $idTokenResponseFactory = new IdTokenResponseFactory( $userRepository, - $this->services[ConfigurationService::class], $this->services[IdTokenBuilder::class], $privateKey, $encryptionKey @@ -254,7 +247,6 @@ public function __construct() $refreshTokenDuration, $authCodeDuration, $requestRuleManager, - $configurationService ); $this->services[AuthCodeGrant::class] = $authCodeGrantFactory->build(); @@ -291,7 +283,7 @@ public function __construct() ); $this->services[AuthorizationServer::class] = $authorizationServerFactory->build(); - $bearerTokenValidator = new BearerTokenValidator($accessTokenRepository); + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepository, $publicKey); $this->services[BearerTokenValidator::class] = $bearerTokenValidator; $resourceServerFactory = new ResourceServerFactory( @@ -305,7 +297,7 @@ public function __construct() /** * @inheritdoc */ - public function get($id) + public function get(string $id): mixed { if (false === $this->has($id)) { throw new class ($id) extends Exception implements NotFoundExceptionInterface { @@ -322,7 +314,7 @@ public function __construct(string $id) /** * @inheritdoc */ - public function has($id): bool + public function has(string $id): bool { return array_key_exists($id, $this->services); } diff --git a/src/Services/DatabaseLegacyOAuth2Import.php b/src/Services/DatabaseLegacyOAuth2Import.php index b14852ea..260478b6 100644 --- a/src/Services/DatabaseLegacyOAuth2Import.php +++ b/src/Services/DatabaseLegacyOAuth2Import.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; } /** - * @psalm-suppress UndefinedClass UndefinedMethod - * - * @return void + * @psalm-suppress UndefinedClass, MixedAssignment, MixedArrayAccess, MixedArgument + * @throws OidcServerException|JsonException */ - public function import() + public function import(): void { if (!class_exists('\SimpleSAML\Modules\OAuth2\Repositories\ClientRepository')) { return; diff --git a/src/Services/DatabaseMigration.php b/src/Services/DatabaseMigration.php index d6a2853a..f3d5da2a 100644 --- a/src/Services/DatabaseMigration.php +++ b/src/Services/DatabaseMigration.php @@ -1,5 +1,7 @@ versionsTableName(); $this->database->write( - "CREATE TABLE IF NOT EXISTS {$versionsTablename} (version VARCHAR(191) PRIMARY KEY NOT NULL)" + "CREATE TABLE IF NOT EXISTS $versionsTablename (version VARCHAR(191) PRIMARY KEY NOT NULL)" ); return $this->database - ->read("SELECT version FROM ${versionsTablename}") + ->read("SELECT version FROM $versionsTablename") ->fetchAll(PDO::FETCH_COLUMN, 0); } @@ -66,57 +68,57 @@ public function migrate(): void if (!in_array('20180305180300', $versions, true)) { $this->version20180305180300(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20180305180300')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20180305180300')"); } if (!in_array('20180425203400', $versions, true)) { $this->version20180425203400(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20180425203400')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20180425203400')"); } if (!in_array('20200517071100', $versions, true)) { $this->version20200517071100(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20200517071100')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20200517071100')"); } if (!in_array('20200901163000', $versions, true)) { $this->version20200901163000(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20200901163000')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20200901163000')"); } if (!in_array('20210714113000', $versions, true)) { $this->version20210714113000(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20210714113000')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20210714113000')"); } if (!in_array('20210823141300', $versions, true)) { $this->version20210823141300(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20210823141300')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20210823141300')"); } if (!in_array('20210827111300', $versions, true)) { $this->version20210827111300(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20210827111300')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20210827111300')"); } if (!in_array('20210902113500', $versions, true)) { $this->version20210902113500(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20210902113500')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20210902113500')"); } if (!in_array('20210908143500', $versions, true)) { $this->version20210908143500(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20210908143500')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20210908143500')"); } if (!in_array('20210916153400', $versions, true)) { $this->version20210916153400(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20210916153400')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20210916153400')"); } if (!in_array('20210916173400', $versions, true)) { $this->version20210916173400(); - $this->database->write("INSERT INTO ${versionsTablename} (version) VALUES ('20210916173400')"); + $this->database->write("INSERT INTO $versionsTablename (version) VALUES ('20210916173400')"); } } @@ -128,11 +130,11 @@ private function versionsTableName(): string /** * @return void */ - private function version20180305180300() + private function version20180305180300(): void { $userTablename = $this->database->applyPrefix(UserRepository::TABLE_NAME); $this->database->write(<<< EOT - CREATE TABLE ${userTablename} ( + CREATE TABLE $userTablename ( id VARCHAR(191) PRIMARY KEY NOT NULL, claims TEXT, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -143,7 +145,7 @@ private function version20180305180300() $clientTableName = $this->database->applyPrefix(ClientRepository::TABLE_NAME); $this->database->write(<<< EOT - CREATE TABLE ${clientTableName} ( + CREATE TABLE $clientTableName ( id VARCHAR(191) PRIMARY KEY NOT NULL, secret VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, @@ -159,17 +161,17 @@ private function version20180305180300() $fkAccessTokenUser = $this->generateIdentifierName([$accessTokenTableName, 'user_id'], 'fk'); $fkAccessTokenClient = $this->generateIdentifierName([$accessTokenTableName, 'client_id'], 'fk'); $this->database->write(<<< EOT - CREATE TABLE ${accessTokenTableName} ( + CREATE TABLE $accessTokenTableName ( id VARCHAR(191) PRIMARY KEY NOT NULL, scopes TEXT, expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, user_id VARCHAR(191) NOT NULL, client_id VARCHAR(191) NOT NULL, is_revoked BOOLEAN NOT NULL DEFAULT false, - CONSTRAINT {$fkAccessTokenUser} FOREIGN KEY (user_id) - REFERENCES ${userTablename} (id) ON DELETE CASCADE, - CONSTRAINT {$fkAccessTokenClient} FOREIGN KEY (client_id) - REFERENCES ${clientTableName} (id) ON DELETE CASCADE + CONSTRAINT $fkAccessTokenUser FOREIGN KEY (user_id) + REFERENCES $userTablename (id) ON DELETE CASCADE, + CONSTRAINT $fkAccessTokenClient FOREIGN KEY (client_id) + REFERENCES $clientTableName (id) ON DELETE CASCADE ) EOT ); @@ -177,13 +179,13 @@ private function version20180305180300() $refreshTokenTableName = $this->database->applyPrefix(RefreshTokenRepository::TABLE_NAME); $fkRefreshTokenAccessToken = $this->generateIdentifierName([$refreshTokenTableName, 'access_token_id'], 'fk'); $this->database->write(<<< EOT - CREATE TABLE ${refreshTokenTableName} ( + CREATE TABLE $refreshTokenTableName ( id VARCHAR(191) PRIMARY KEY NOT NULL, expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, access_token_id VARCHAR(191) NOT NULL, is_revoked BOOLEAN NOT NULL DEFAULT false, - CONSTRAINT {$fkRefreshTokenAccessToken} FOREIGN KEY (access_token_id) - REFERENCES ${accessTokenTableName} (id) ON DELETE CASCADE + CONSTRAINT $fkRefreshTokenAccessToken FOREIGN KEY (access_token_id) + REFERENCES $accessTokenTableName (id) ON DELETE CASCADE ) EOT ); @@ -192,7 +194,7 @@ private function version20180305180300() $fkAuthCodeUser = $this->generateIdentifierName([$authCodeTableName, 'user_id'], 'fk'); $fkAuthCodeClient = $this->generateIdentifierName([$authCodeTableName, 'client_id'], 'fk'); $this->database->write(<<< EOT - CREATE TABLE ${authCodeTableName} ( + CREATE TABLE $authCodeTableName ( id VARCHAR(191) PRIMARY KEY NOT NULL, scopes TEXT, expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -200,10 +202,10 @@ private function version20180305180300() client_id VARCHAR(191) NOT NULL, is_revoked BOOLEAN NOT NULL DEFAULT false, redirect_uri TEXT NOT NULL, - CONSTRAINT {$fkAuthCodeUser} FOREIGN KEY (user_id) - REFERENCES ${userTablename} (id) ON DELETE CASCADE, - CONSTRAINT {$fkAuthCodeClient} FOREIGN KEY (client_id) - REFERENCES ${clientTableName} (id) ON DELETE CASCADE + CONSTRAINT $fkAuthCodeUser FOREIGN KEY (user_id) + REFERENCES $userTablename (id) ON DELETE CASCADE, + CONSTRAINT $fkAuthCodeClient FOREIGN KEY (client_id) + REFERENCES $clientTableName (id) ON DELETE CASCADE ) EOT ); @@ -212,7 +214,7 @@ private function version20180305180300() /** * @return void */ - private function version20180425203400() + private function version20180425203400(): void { $clientTableName = $this->database->applyPrefix(ClientRepository::TABLE_NAME); $this->database->write(<<< EOT @@ -222,7 +224,7 @@ private function version20180425203400() ); } - private function version20200517071100() + private function version20200517071100(): void { $clientTableName = $this->database->applyPrefix(ClientRepository::TABLE_NAME); $this->database->write(<<< EOT @@ -255,7 +257,7 @@ private function version20210902113500(): void /** * Add auth_code_id column to access token and refresh token tables */ - protected function version20210714113000() + protected function version20210714113000(): void { $tableName = $this->database->applyPrefix(AccessTokenRepository::TABLE_NAME); $this->database->write(<<< EOT @@ -275,7 +277,7 @@ protected function version20210714113000() /** * Add requested claims to authorization token */ - protected function version20210823141300() + protected function version20210823141300(): void { $tableName = $this->database->applyPrefix(AccessTokenRepository::TABLE_NAME); $this->database->write(<<< EOT @@ -296,12 +298,12 @@ protected function version20210827111300(): void $fkAllowedOriginClient = $this->generateIdentifierName([$allowedOriginTableName, 'client_id'], 'fk'); $this->database->write(<<< EOT - CREATE TABLE ${allowedOriginTableName} ( + CREATE TABLE $allowedOriginTableName ( client_id VARCHAR(191) NOT NULL, origin VARCHAR(191) NOT NULL, - CONSTRAINT {$pkAllowedOriginClient} PRIMARY KEY (client_id, origin), - CONSTRAINT {$fkAllowedOriginClient} FOREIGN KEY (client_id) - REFERENCES ${clientTableName} (id) ON DELETE CASCADE + CONSTRAINT $pkAllowedOriginClient PRIMARY KEY (client_id, origin), + CONSTRAINT $fkAllowedOriginClient FOREIGN KEY (client_id) + REFERENCES $clientTableName (id) ON DELETE CASCADE ) EOT ); @@ -323,7 +325,7 @@ protected function version20210908143500(): void /** * Add backchannel_logout_uri to client */ - protected function version20210916153400() + protected function version20210916153400(): void { $clientTableName = $this->database->applyPrefix(ClientRepository::TABLE_NAME); $this->database->write(<<< EOT @@ -336,12 +338,12 @@ protected function version20210916153400() /** * Add logout_ticket table */ - protected function version20210916173400() + protected function version20210916173400(): void { - $tableName = $this->database->applyPrefix(SessionLogoutTicketStoreDb::TABLE_NAME); + $tableName = $this->database->applyPrefix(LogoutTicketStoreDb::TABLE_NAME); $this->database->write( <<< EOT - CREATE TABLE ${tableName} ( + CREATE TABLE $tableName ( sid VARCHAR(191) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) @@ -349,12 +351,13 @@ protected function version20210916173400() ); } + /** + * @param string[] $columnNames + */ private function generateIdentifierName(array $columnNames, string $prefix = '', int $maxSize = 30): string { - $hash = implode('', array_map(function ($column) { - return dechex(crc32($column)); - }, $columnNames)); + $hash = implode('', array_map(fn($column) => dechex(crc32($column)), $columnNames)); - return mb_strtoupper(mb_substr("{$prefix}_{$hash}", 0, $maxSize)); + return mb_strtoupper(mb_substr("{$prefix}_$hash", 0, $maxSize)); } } diff --git a/src/Services/IdTokenBuilder.php b/src/Services/IdTokenBuilder.php index 6e68873d..ad03b38c 100644 --- a/src/Services/IdTokenBuilder.php +++ b/src/Services/IdTokenBuilder.php @@ -1,5 +1,7 @@ jsonWebTokenBuilderService = $jsonWebTokenBuilderService; - $this->claimExtractor = $claimExtractor; } /** @@ -85,29 +82,42 @@ public function build( ); $claims = array_merge($additionalClaims, $claims); - + /** + * @var string $claimName + * @var mixed $claimValue + */ foreach ($claims as $claimName => $claimValue) { switch ($claimName) { case RegisteredClaims::AUDIENCE: - $builder->permittedFor($claimValue); + if (is_array($claimValue)) { + /** @psalm-suppress MixedAssignment */ + foreach ($claimValue as $aud) { + $builder->permittedFor((string)$aud); + } + } else { + $builder->permittedFor((string)$claimValue); + } break; case RegisteredClaims::EXPIRATION_TIME: - $builder->expiresAt(new DateTimeImmutable('@' . $claimValue)); + /** @noinspection PhpUnnecessaryStringCastInspection */ + $builder->expiresAt(new DateTimeImmutable('@' . (string)$claimValue)); break; case RegisteredClaims::ID: - $builder->identifiedBy($claimValue); + $builder->identifiedBy((string)$claimValue); break; case RegisteredClaims::ISSUED_AT: - $builder->issuedAt(new DateTimeImmutable('@' . $claimValue)); + /** @noinspection PhpUnnecessaryStringCastInspection */ + $builder->issuedAt(new DateTimeImmutable('@' . (string)$claimValue)); break; case RegisteredClaims::ISSUER: - $builder->issuedBy($claimValue); + $builder->issuedBy((string)$claimValue); break; case RegisteredClaims::NOT_BEFORE: - $builder->canOnlyBeUsedAfter(new DateTimeImmutable('@' . $claimValue)); + /** @noinspection PhpUnnecessaryStringCastInspection */ + $builder->canOnlyBeUsedAfter(new DateTimeImmutable('@' . (string)$claimValue)); break; case RegisteredClaims::SUBJECT: - $builder->relatedTo($claimValue); + $builder->relatedTo((string)$claimValue); break; default: if ($addClaimsFromScopes || array_key_exists($claimName, $additionalClaims)) { @@ -119,6 +129,9 @@ public function build( return $this->jsonWebTokenBuilderService->getSignedJwtTokenFromBuilder($builder); } + /** + * @throws OAuthServerException + */ protected function getBuilder( AccessTokenEntityInterface $accessToken, UserEntityInterface $userEntity @@ -129,13 +142,11 @@ protected function getBuilder( ->identifiedBy($accessToken->getIdentifier()) ->canOnlyBeUsedAfter(new DateTimeImmutable('now')) ->expiresAt($accessToken->getExpiryDateTime()) - ->relatedTo($userEntity->getIdentifier()); + ->relatedTo((string)$userEntity->getIdentifier()); } /** - * @param AccessTokenEntityInterface $accessToken * @param string $jwsAlgorithm JWS Algorithm designation (like RS256, RS384...) - * @return string */ protected function generateAccessTokenHash(AccessTokenEntityInterface $accessToken, string $jwsAlgorithm): string { @@ -152,8 +163,8 @@ protected function generateAccessTokenHash(AccessTokenEntityInterface $accessTok EntityStringRepresentationInterface::class); } - // Try to use toString() so that it uses the string representation if it was already casted to string, - // otherwise, use the casted version. + // Try to use toString() so that it uses the string representation if it was already cast to string, + // otherwise, use the cast version. $accessTokenString = $accessToken->toString() ?? (string) $accessToken; $hashAlgorithm = 'sha' . $jwsAlgorithmBitLength; diff --git a/src/Services/JsonWebKeySetService.php b/src/Services/JsonWebKeySetService.php index 450766b7..74d24158 100644 --- a/src/Services/JsonWebKeySetService.php +++ b/src/Services/JsonWebKeySetService.php @@ -1,5 +1,7 @@ getCertPath(); + $publicKeyPath = $moduleConfig->getCertPath(); if (!file_exists($publicKeyPath)) { - throw new Exception("OpenId Connect certification file does not exists: {$publicKeyPath}."); + throw new Exception("OpenId Connect certification file does not exists: $publicKeyPath."); } $kid = FingerprintGenerator::forFile($publicKeyPath); @@ -50,9 +49,9 @@ public function __construct(ConfigurationService $configurationService) } /** - * @return \Jose\Component\Core\JWK[] + * @return JWK[] */ - public function keys() + public function keys(): array { return $this->jwkSet->all(); } diff --git a/src/Services/JsonWebTokenBuilderService.php b/src/Services/JsonWebTokenBuilderService.php index 3a57297f..fff9e447 100644 --- a/src/Services/JsonWebTokenBuilderService.php +++ b/src/Services/JsonWebTokenBuilderService.php @@ -1,5 +1,7 @@ configurationService = $configurationService ?? new ConfigurationService(); - $this->jwtConfig = Configuration::forAsymmetricSigner( - $this->configurationService->getSigner(), + $this->moduleConfig->getSigner(), InMemory::file( - $this->configurationService->getPrivateKeyPath(), - $this->configurationService->getPrivateKeyPassPhrase() ?? '' + $this->moduleConfig->getPrivateKeyPath(), + $this->moduleConfig->getPrivateKeyPassPhrase() ?? '' ), - InMemory::empty() + InMemory::plainText('empty', 'empty') ); } @@ -46,14 +46,17 @@ public function getDefaultJwtTokenBuilder(): Builder { // Ignore microseconds when handling dates. return $this->jwtConfig->builder(ChainedFormatter::withUnixTimestampDates()) - ->issuedBy($this->configurationService->getSimpleSAMLSelfURLHost()) + ->issuedBy($this->moduleConfig->getSimpleSAMLSelfURLHost()) ->issuedAt(new DateTimeImmutable('now')) ->identifiedBy(UniqueIdentifierGenerator::hitMe()); } + /** + * @throws Exception + */ public function getSignedJwtTokenFromBuilder(Builder $builder): UnencryptedToken { - $kid = FingerprintGenerator::forFile($this->configurationService->getCertPath()); + $kid = FingerprintGenerator::forFile($this->moduleConfig->getCertPath()); return $builder->withHeader('kid', $kid) ->getToken( @@ -67,6 +70,6 @@ public function getSignedJwtTokenFromBuilder(Builder $builder): UnencryptedToken */ public function getSigner(): Signer { - return $this->configurationService->getSigner(); + return $this->moduleConfig->getSigner(); } } diff --git a/src/Services/LoggerService.php b/src/Services/LoggerService.php index faebeaa3..7045d6eb 100644 --- a/src/Services/LoggerService.php +++ b/src/Services/LoggerService.php @@ -1,40 +1,43 @@ alert($message, $context); - break; - case LogLevel::CRITICAL: - $this->critical($message, $context); - break; - case LogLevel::DEBUG: - $this->debug($message, $context); - break; - case LogLevel::EMERGENCY: - $this->emergency($message, $context); - break; - case LogLevel::ERROR: - $this->error($message, $context); - break; - case LogLevel::INFO: - $this->info($message, $context); - break; - case LogLevel::NOTICE: - $this->notice($message, $context); - break; - case LogLevel::WARNING: - $this->warning($message, $context); - break; - default: - throw new InvalidArgumentException("Unrecognized log level '$level''"); - } + match ($level) { + LogLevel::ALERT => $this->alert($message, $context), + LogLevel::CRITICAL => $this->critical($message, $context), + LogLevel::DEBUG => $this->debug($message, $context), + LogLevel::EMERGENCY => $this->emergency($message, $context), + LogLevel::ERROR => $this->error($message, $context), + LogLevel::INFO => $this->info($message, $context), + LogLevel::NOTICE => $this->notice($message, $context), + LogLevel::WARNING => $this->warning($message, $context), + default => throw new InvalidArgumentException("Unrecognized log level '$level''"), + }; } public static function getInstance(): self diff --git a/src/Services/LogoutTokenBuilder.php b/src/Services/LogoutTokenBuilder.php index 4f522251..23c9746a 100644 --- a/src/Services/LogoutTokenBuilder.php +++ b/src/Services/LogoutTokenBuilder.php @@ -1,23 +1,23 @@ jsonWebTokenBuilderService = $jsonWebTokenBuilderService ?? new JsonWebTokenBuilderService(); } /** - * @throws OAuthServerException + * @throws OAuthServerException|Exception */ public function forRelyingPartyAssociation(RelyingPartyAssociationInterface $relyingPartyAssociation): string { diff --git a/src/Services/OidcOpenIdProviderMetadataService.php b/src/Services/OpMetadataService.php similarity index 60% rename from src/Services/OidcOpenIdProviderMetadataService.php rename to src/Services/OpMetadataService.php index 63642383..d9d393b1 100644 --- a/src/Services/OidcOpenIdProviderMetadataService.php +++ b/src/Services/OpMetadataService.php @@ -1,28 +1,28 @@ configurationService = $configurationService; $this->initMetadata(); } @@ -30,17 +30,17 @@ public function __construct( * Initialize metadata array. * @throws Exception */ - public function initMetadata(): void + private function initMetadata(): void { $this->metadata = []; - $this->metadata['issuer'] = $this->configurationService->getSimpleSAMLSelfURLHost(); + $this->metadata['issuer'] = $this->moduleConfig->getSimpleSAMLSelfURLHost(); $this->metadata['authorization_endpoint'] = - $this->configurationService->getOpenIdConnectModuleURL('authorize.php'); - $this->metadata['token_endpoint'] = $this->configurationService->getOpenIdConnectModuleURL('token.php'); - $this->metadata['userinfo_endpoint'] = $this->configurationService->getOpenIdConnectModuleURL('userinfo.php'); - $this->metadata['end_session_endpoint'] = $this->configurationService->getOpenIdConnectModuleURL('logout.php'); - $this->metadata['jwks_uri'] = $this->configurationService->getOpenIdConnectModuleURL('jwks.php'); - $this->metadata['scopes_supported'] = array_keys($this->configurationService->getOpenIDScopes()); + $this->moduleConfig->getOpenIdConnectModuleURL('authorize.php'); + $this->metadata['token_endpoint'] = $this->moduleConfig->getOpenIdConnectModuleURL('token.php'); + $this->metadata['userinfo_endpoint'] = $this->moduleConfig->getOpenIdConnectModuleURL('userinfo.php'); + $this->metadata['end_session_endpoint'] = $this->moduleConfig->getOpenIdConnectModuleURL('logout.php'); + $this->metadata['jwks_uri'] = $this->moduleConfig->getOpenIdConnectModuleURL('jwks.php'); + $this->metadata['scopes_supported'] = array_keys($this->moduleConfig->getOpenIDScopes()); $this->metadata['response_types_supported'] = ['code', 'token', 'id_token', 'id_token token']; $this->metadata['subject_types_supported'] = ['public']; $this->metadata['id_token_signing_alg_values_supported'] = ['RS256']; @@ -49,7 +49,7 @@ public function initMetadata(): void $this->metadata['request_parameter_supported'] = false; $this->metadata['grant_types_supported'] = ['authorization_code', 'refresh_token']; $this->metadata['claims_parameter_supported'] = true; - if (!(empty($acrValuesSupported = $this->configurationService->getAcrValuesSupported()))) { + if (!(empty($acrValuesSupported = $this->moduleConfig->getAcrValuesSupported()))) { $this->metadata['acr_values_supported'] = $acrValuesSupported; } $this->metadata['backchannel_logout_supported'] = true; diff --git a/src/Services/RoutingService.php b/src/Services/RoutingService.php index 4727214d..571c8936 100644 --- a/src/Services/RoutingService.php +++ b/src/Services/RoutingService.php @@ -1,5 +1,7 @@ requireAdmin(); } @@ -51,10 +60,14 @@ public static function call(string $controllerClassname, bool $authenticated = t } /** + * @throws BadRequest + * @throws ContainerExceptionInterface * @throws Exception + * @throws NotFoundExceptionInterface + * @throws ReflectionException * @throws \Exception */ - public static function callWithPermission(string $controllerClassname, string $permission) + public static function callWithPermission(string $controllerClassname, string $permission): void { $container = new Container(); /** @var AuthContextService $authContext */ @@ -66,9 +79,13 @@ public static function callWithPermission(string $controllerClassname, string $p /** * @throws BadRequest * @throws Exception + * @throws ReflectionException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws \Exception + * @psalm-suppress MixedMethodCall, MixedAssignment */ - private static function callController($container, string $controllerClassname): void + private static function callController(ContainerInterface $container, string $controllerClassname): void { /** @var callable $controller */ $controller = self::getController($controllerClassname, $container); @@ -105,17 +122,20 @@ private static function callController($container, string $controllerClassname): return; } - throw new Exception('Response type not supported: ' . get_class($response)); + throw new Exception('Response type not supported: ' . $response::class); } /** - * @throws ReflectionException * @throws BadRequest + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws ReflectionException + * @psalm-suppress MixedAssignment */ protected static function getController(string $controllerClassname, ContainerInterface $container): object { if (!class_exists($controllerClassname)) { - throw new BadRequest("Controller does not exist: {$controllerClassname}"); + throw new BadRequest("Controller does not exist: $controllerClassname"); } $controllerReflectionClass = new ReflectionClass($controllerClassname); @@ -144,12 +164,13 @@ protected static function getController(string $controllerClassname, ContainerIn /** * @return void */ - protected static function enableJsonExceptionResponse() + protected static function enableJsonExceptionResponse(): void { set_exception_handler(function (Throwable $t) { if ($t instanceof Error) { // Showing SSP Error will also use SSP logger to log it. - return $t->show(); + $t->show(); + return; } elseif ($t instanceof OAuthServerException) { $response = $t->generateHttpResponse(new Response()); } else { diff --git a/src/Services/SessionMessagesService.php b/src/Services/SessionMessagesService.php index 039554ca..799825a8 100644 --- a/src/Services/SessionMessagesService.php +++ b/src/Services/SessionMessagesService.php @@ -1,5 +1,7 @@ session = $session; } /** - * @return void + * @throws Exception */ - public function addMessage(string $value) + public function addMessage(string $value): void { $this->session->setData('message', uniqid(), $value); } @@ -37,11 +36,12 @@ public function addMessage(string $value) /** * @return array */ - public function getMessages() + public function getMessages(): array { + /** @var array $messages */ $messages = $this->session->getDataOfType('message'); - foreach ($messages as $key => $message) { + foreach (array_keys($messages) as $key) { $this->session->deleteData('message', $key); } diff --git a/src/Services/SessionService.php b/src/Services/SessionService.php index fea55204..6cdb52ec 100644 --- a/src/Services/SessionService.php +++ b/src/Services/SessionService.php @@ -1,5 +1,7 @@ session = $session; } public function getCurrentSession(): Session @@ -50,10 +50,17 @@ public function setIsCookieBasedAuthn(bool $isCookieBasedAuthn): void public function getIsCookieBasedAuthn(): ?bool { - return $this->session->getData( + /** @var ?bool $isCookieBasedAuthn */ + $isCookieBasedAuthn = $this->session->getData( self::SESSION_DATA_TYPE, self::SESSION_DATA_ID_IS_COOKIE_BASED_AUTHN ); + + if (is_bool($isCookieBasedAuthn)) { + return $isCookieBasedAuthn; + } + + return null; } /** @@ -61,7 +68,12 @@ public function getIsCookieBasedAuthn(): ?bool */ public function addRelyingPartyAssociation(RelyingPartyAssociationInterface $association): void { - $associationId = hash('sha256', $association->getClientId() . $association->getSessionId()); + $sessionId = $association->getSessionId(); + if (empty($sessionId)) { + return; + } + + $associationId = hash('sha256', $association->getClientId() . $sessionId); $associations = $this->getRelyingPartyAssociations(); if (! array_key_exists($associationId, $associations)) { @@ -81,9 +93,22 @@ public function getRelyingPartyAssociations(): array return self::getRelyingPartyAssociationsForSession($this->session); } + /** + * @return array + */ public static function getRelyingPartyAssociationsForSession(Session $session): array { - return $session->getData(self::SESSION_DATA_TYPE, self::SESSION_DATA_ID_RP_ASSOCIATIONS) ?? []; + $relyingPartyAssociations = $session->getData(self::SESSION_DATA_TYPE, self::SESSION_DATA_ID_RP_ASSOCIATIONS); + + if (!is_array($relyingPartyAssociations)) { + return []; + } + + // Make sure we only have RelyingPartyAssociations here... + return array_filter( + $relyingPartyAssociations, + fn($value) => $value instanceof RelyingPartyAssociationInterface + ); } /** @@ -138,7 +163,6 @@ public function registerLogoutHandler(string $authSourceId, string $className, s /** * Set indication if logout was initiated using OIDC protocol. - * @param bool $isOidcInitiatedLogout * @throws Exception */ public function setIsOidcInitiatedLogout(bool $isOidcInitiatedLogout): void @@ -151,19 +175,8 @@ public function setIsOidcInitiatedLogout(bool $isOidcInitiatedLogout): void ); } - /** - * Get indication if logout was initiated using OIDC protocol. - * @return bool - */ - public function getIsOidcInitiatedLogout(): bool - { - return self::getIsOidcInitiatedLogoutForSession($this->session); - } - /** * Helper method to get indication if logout was initiated using OIDC protocol for given session. - * @param Session $session - * @return bool */ public static function getIsOidcInitiatedLogoutForSession(Session $session): bool { diff --git a/src/Store/SessionLogoutTicketStoreBuilder.php b/src/Store/SessionLogoutTicketStoreBuilder.php deleted file mode 100644 index 53e90d18..00000000 --- a/src/Store/SessionLogoutTicketStoreBuilder.php +++ /dev/null @@ -1,29 +0,0 @@ -database = $database ?? Database::getInstance(); - $this->ttl = $ttl >= 0 ? $ttl : 0; + $this->ttl = max($ttl, 0); } public function add(string $sid): void @@ -52,6 +54,7 @@ public function delete(string $sid): void } /** + * @inheritDoc * @throws Exception */ public function deleteMultiple(array $sids): void diff --git a/src/Stores/Session/LogoutTicketStoreInterface.php b/src/Stores/Session/LogoutTicketStoreInterface.php new file mode 100644 index 00000000..a961ad06 --- /dev/null +++ b/src/Stores/Session/LogoutTicketStoreInterface.php @@ -0,0 +1,17 @@ +add($rule); } $this->resultBag = new ResultBag(); - $this->loggerService = $loggerService ?? new LoggerService(); } public function add(RequestRuleInterface $rule): void @@ -49,12 +48,10 @@ public function add(RequestRuleInterface $rule): void } /** - * @param ServerRequestInterface $request - * @param array $ruleKeysToExecute + * @param class-string[] $ruleKeysToExecute * @param bool $useFragmentInHttpErrorResponses Indicate that in case of HTTP error responses, params should be * returned in URI fragment instead of query. - * @param array $allowedServerRequestMethods Indicate allowed HTTP methods used for request - * @return ResultBagInterface + * @param string[] $allowedServerRequestMethods Indicate allowed HTTP methods used for request * @throws OidcServerException */ public function check( @@ -86,8 +83,7 @@ public function check( } /** - * Predefine (add) the existing result so it can be used by other checkers during check. - * @param ResultInterface $result + * Predefine (add) the existing result, so it can be used by other checkers during check. */ public function predefineResult(ResultInterface $result): void { @@ -96,7 +92,6 @@ public function predefineResult(ResultInterface $result): void /** * Predefine existing ResultBag so that it can be used by other checkers during check. - * @param ResultBagInterface $resultBag */ public function predefineResultBag(ResultBagInterface $resultBag): void { @@ -105,10 +100,8 @@ public function predefineResultBag(ResultBagInterface $resultBag): void /** * Set data which will be available in each check, using key value pair - * @param string $key - * @param mixed $value */ - public function setData(string $key, $value): void + public function setData(string $key, mixed $value): void { $this->data[$key] = $value; } diff --git a/src/Utils/Checker/Result.php b/src/Utils/Checker/Result.php index 74d14d2c..dfb8a776 100644 --- a/src/Utils/Checker/Result.php +++ b/src/Utils/Checker/Result.php @@ -1,30 +1,19 @@ key = $key; - $this->value = $value; } public function getKey(): string @@ -32,12 +21,12 @@ public function getKey(): string return $this->key; } - public function getValue() + public function getValue(): mixed { return $this->value; } - public function setValue($value): void + public function setValue(mixed $value): void { $this->value = $value; } diff --git a/src/Utils/Checker/ResultBag.php b/src/Utils/Checker/ResultBag.php index 3227f069..6d506cf9 100644 --- a/src/Utils/Checker/ResultBag.php +++ b/src/Utils/Checker/ResultBag.php @@ -1,16 +1,21 @@ results[$key])) { - return $this->results[$key]; - } - - return null; + return $this->results[$key] ?? null; } /** @@ -42,7 +43,7 @@ public function getOrFail(string $key): ResultInterface $result = $this->get($key); if ($result === null) { - throw new \LogicException(\sprintf('Checker error: expected existing result, but none found (%s)', $key)); + throw new LogicException(sprintf('Checker error: expected existing result, but none found (%s)', $key)); } return $result; diff --git a/src/Utils/Checker/Rules/AbstractRule.php b/src/Utils/Checker/Rules/AbstractRule.php index 3c9bb356..bd7727cf 100644 --- a/src/Utils/Checker/Rules/AbstractRule.php +++ b/src/Utils/Checker/Rules/AbstractRule.php @@ -1,13 +1,12 @@ getQueryParams()[$paramKey] ?? null; + $param = isset($request->getQueryParams()[$paramKey]) ? + (string)$request->getQueryParams()[$paramKey] : null; + break; case 'POST': if (is_array($parsedBody = $request->getParsedBody())) { - return $parsedBody[$paramKey] ?? null; + $param = isset($parsedBody[$paramKey]) ? (string)$parsedBody[$paramKey] : null; } - // break; // ... falls through to default + break; default: $loggerService->warning( sprintf( @@ -57,6 +63,6 @@ protected function getParamFromRequestBasedOnAllowedMethods( ); } - return null; + return $param; } } diff --git a/src/Utils/Checker/Rules/AcrValuesRule.php b/src/Utils/Checker/Rules/AcrValuesRule.php index d29f7d9b..ffc6ed1d 100644 --- a/src/Utils/Checker/Rules/AcrValuesRule.php +++ b/src/Utils/Checker/Rules/AcrValuesRule.php @@ -1,5 +1,7 @@ getValue()['id_token']['acr'] ?? []; - $acrValues['essential'] = $requestedAcrClaim['essential'] ?? false; + $acrValues['essential'] = (bool)($requestedAcrClaim['essential'] ?? false); $acrValues['values'] = array_merge( isset($requestedAcrClaim['value']) ? [$requestedAcrClaim['value']] : [], - $requestedAcrClaim['values'] ?? [] + isset($requestedAcrClaim['values']) && is_array($requestedAcrClaim['values']) ? + $requestedAcrClaim['values'] : [] ); } // Check for acr_values request parameter - if (($acrValuesParam = $request->getQueryParams()['acr_values'] ?? null) !== null) { + /** @var ?string $acrValuesParam */ + $acrValuesParam = $request->getQueryParams()['acr_values'] ?? null; + if ($acrValuesParam !== null) { $acrValues['values'] = array_merge($acrValues['values'], explode(' ', $acrValuesParam)); } diff --git a/src/Utils/Checker/Rules/AddClaimsToIdTokenRule.php b/src/Utils/Checker/Rules/AddClaimsToIdTokenRule.php index 7f8620c7..60bff15b 100644 --- a/src/Utils/Checker/Rules/AddClaimsToIdTokenRule.php +++ b/src/Utils/Checker/Rules/AddClaimsToIdTokenRule.php @@ -1,5 +1,7 @@ getOrFail(ResponseTypeRule::class)->getValue(); return new Result($this->getKey(), $responseType === "id_token"); diff --git a/src/Utils/Checker/Rules/ClientIdRule.php b/src/Utils/Checker/Rules/ClientIdRule.php index 27a0b9b3..88d475a9 100644 --- a/src/Utils/Checker/Rules/ClientIdRule.php +++ b/src/Utils/Checker/Rules/ClientIdRule.php @@ -1,10 +1,12 @@ clientRepository = $clientRepository; } /** @@ -31,6 +30,7 @@ public function checkRule( bool $useFragmentInHttpErrorResponses = false, array $allowedServerRequestMethods = ['GET'] ): ?ResultInterface { + /** @var ?string $clientId */ $clientId = $request->getQueryParams()['client_id'] ?? $request->getServerParams()['PHP_AUTH_USER'] ?? null; if ($clientId === null) { diff --git a/src/Utils/Checker/Rules/CodeChallengeMethodRule.php b/src/Utils/Checker/Rules/CodeChallengeMethodRule.php index b47f448b..31e975a0 100644 --- a/src/Utils/Checker/Rules/CodeChallengeMethodRule.php +++ b/src/Utils/Checker/Rules/CodeChallengeMethodRule.php @@ -1,5 +1,7 @@ codeChallengeVerifiersRepository = $codeChallengeVerifiersRepository; } /** @@ -37,6 +36,7 @@ public function checkRule( /** @var string|null $state */ $state = $currentResultBag->getOrFail(StateRule::class)->getValue(); + /** @var string $codeChallengeMethod */ $codeChallengeMethod = $request->getQueryParams()['code_challenge_method'] ?? 'plain'; $codeChallengeVerifiers = $this->codeChallengeVerifiersRepository->getAll(); @@ -44,9 +44,7 @@ public function checkRule( throw OidcServerException::invalidRequest( 'code_challenge_method', 'Code challenge method must be one of ' . implode(', ', array_map( - function ($method) { - return '`' . $method . '`'; - }, + fn($method) => '`' . $method . '`', array_keys($codeChallengeVerifiers) )), null, diff --git a/src/Utils/Checker/Rules/CodeChallengeRule.php b/src/Utils/Checker/Rules/CodeChallengeRule.php index 0c0d53ef..369695a1 100644 --- a/src/Utils/Checker/Rules/CodeChallengeRule.php +++ b/src/Utils/Checker/Rules/CodeChallengeRule.php @@ -1,5 +1,7 @@ getOrFail(StateRule::class)->getValue(); + /** @var ?string $codeChallenge */ $codeChallenge = $request->getQueryParams()['code_challenge'] ?? null; if ($codeChallenge === null) { diff --git a/src/Utils/Checker/Rules/IdTokenHintRule.php b/src/Utils/Checker/Rules/IdTokenHintRule.php index 8e9bb631..e0f65ab2 100644 --- a/src/Utils/Checker/Rules/IdTokenHintRule.php +++ b/src/Utils/Checker/Rules/IdTokenHintRule.php @@ -1,5 +1,7 @@ configurationService = $configurationService; - $this->cryptKeyFactory = $cryptKeyFactory; } /** @@ -60,7 +57,7 @@ public function checkRule( $publicKey = $this->cryptKeyFactory->buildPublicKey(); /** @psalm-suppress ArgumentTypeCoercion */ $jwtConfig = Configuration::forAsymmetricSigner( - $this->configurationService->getSigner(), + $this->moduleConfig->getSigner(), InMemory::plainText($privateKey->getKeyContents(), $privateKey->getPassPhrase() ?? ''), InMemory::plainText($publicKey->getKeyContents()) ); @@ -72,12 +69,12 @@ public function checkRule( /** @psalm-suppress ArgumentTypeCoercion */ $jwtConfig->validator()->assert( $idTokenHint, - new IssuedBy($this->configurationService->getSimpleSAMLSelfURLHost()), + new IssuedBy($this->moduleConfig->getSimpleSAMLSelfURLHost()), // Note: although logout spec does not mention it, validating signature seems like an important check // to make. However, checking the signature in a key roll-over scenario will fail for ID tokens // signed with previous key... new SignedWith( - $this->configurationService->getSigner(), + $this->moduleConfig->getSigner(), InMemory::plainText($publicKey->getKeyContents()) ) ); diff --git a/src/Utils/Checker/Rules/MaxAgeRule.php b/src/Utils/Checker/Rules/MaxAgeRule.php index 76a8c45d..e1ef9769 100644 --- a/src/Utils/Checker/Rules/MaxAgeRule.php +++ b/src/Utils/Checker/Rules/MaxAgeRule.php @@ -1,9 +1,11 @@ authSimpleFactory = $authSimpleFactory; - $this->authenticationService = $authenticationService; } /** @@ -58,6 +54,8 @@ public function checkRule( /** @var string $redirectUri */ $redirectUri = $currentResultBag->getOrFail(RedirectUriRule::class)->getValue(); + /** @var ?string $state */ + $state = $queryParams['state'] ?? null; if (false === filter_var($queryParams['max_age'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) { throw OidcServerException::invalidRequest( @@ -65,7 +63,7 @@ public function checkRule( 'max_age must be a valid integer', null, $redirectUri, - $queryParams['state'] ?? null, + $state, $useFragmentInHttpErrorResponses ); } diff --git a/src/Utils/Checker/Rules/PostLogoutRedirectUriRule.php b/src/Utils/Checker/Rules/PostLogoutRedirectUriRule.php index 29b78615..8b8fc5eb 100644 --- a/src/Utils/Checker/Rules/PostLogoutRedirectUriRule.php +++ b/src/Utils/Checker/Rules/PostLogoutRedirectUriRule.php @@ -1,5 +1,7 @@ clientRepository = $clientRepository; } /** @@ -59,9 +58,10 @@ public function checkRule( $claims = $idTokenHint->claims()->all(); - if (! isset($claims['aud']) || empty($claims['aud'])) { + if (empty($claims['aud'])) { throw OidcServerException::invalidRequest('id_token_hint', 'aud claim not present', null, null, $state); } + /** @var string[] $auds */ $auds = is_array($claims['aud']) ? $claims['aud'] : [$claims['aud']]; $isPostLogoutRedirectUriRegistered = false; diff --git a/src/Utils/Checker/Rules/PromptRule.php b/src/Utils/Checker/Rules/PromptRule.php index db115032..feb57b87 100644 --- a/src/Utils/Checker/Rules/PromptRule.php +++ b/src/Utils/Checker/Rules/PromptRule.php @@ -1,10 +1,12 @@ authSimpleFactory = $authSimpleFactory; - $this->authenticationService = $authenticationService; } /** @@ -56,19 +52,21 @@ public function checkRule( return null; } - $prompt = explode(" ", $queryParams['prompt']); + $prompt = explode(" ", (string)$queryParams['prompt']); if (count($prompt) > 1 && in_array('none', $prompt, true)) { throw OAuthServerException::invalidRequest('prompt', 'Invalid prompt parameter'); } /** @var string $redirectUri */ $redirectUri = $currentResultBag->getOrFail(RedirectUriRule::class)->getValue(); + /** @var ?string $state */ + $state = $queryParams['state'] ?? null; if (in_array('none', $prompt, true) && !$authSimple->isAuthenticated()) { throw OidcServerException::loginRequired( null, $redirectUri, null, - $queryParams['state'] ?? null, + $state, $useFragmentInHttpErrorResponses ); } diff --git a/src/Utils/Checker/Rules/RedirectUriRule.php b/src/Utils/Checker/Rules/RedirectUriRule.php index eb411c51..bfe5e926 100644 --- a/src/Utils/Checker/Rules/RedirectUriRule.php +++ b/src/Utils/Checker/Rules/RedirectUriRule.php @@ -1,10 +1,12 @@ getRedirectUri()) && - (strcmp($client->getRedirectUri(), $redirectUri) !== 0) - ) { + $clientRedirectUri = $client->getRedirectUri(); + if (is_string($clientRedirectUri) && (strcmp($clientRedirectUri, $redirectUri) !== 0)) { throw OidcServerException::invalidClient($request); } elseif ( - is_array($client->getRedirectUri()) && - in_array($redirectUri, $client->getRedirectUri(), true) === false + is_array($clientRedirectUri) && + in_array($redirectUri, $clientRedirectUri, true) === false ) { throw OidcServerException::invalidRequest('redirect_uri'); } diff --git a/src/Utils/Checker/Rules/RequestParameterRule.php b/src/Utils/Checker/Rules/RequestParameterRule.php index 48ce9def..8debca13 100644 --- a/src/Utils/Checker/Rules/RequestParameterRule.php +++ b/src/Utils/Checker/Rules/RequestParameterRule.php @@ -1,5 +1,7 @@ getOrFail(RedirectUriRule::class)->getValue(); - $state = $currentResultBag->get(StateRule::class); + /** @var ?string $stateValue */ + $stateValue = ($currentResultBag->get(StateRule::class))?->getValue(); throw OidcServerException::requestNotSupported( 'request object not supported', $redirectUri, null, - $state ? $state->getValue() : null, + $stateValue, $useFragmentInHttpErrorResponses ); } diff --git a/src/Utils/Checker/Rules/RequestedClaimsRule.php b/src/Utils/Checker/Rules/RequestedClaimsRule.php index 597868bb..2f92616a 100644 --- a/src/Utils/Checker/Rules/RequestedClaimsRule.php +++ b/src/Utils/Checker/Rules/RequestedClaimsRule.php @@ -1,24 +1,22 @@ claimExtractor = $claimExtractor; } @@ -33,11 +31,13 @@ public function checkRule( bool $useFragmentInHttpErrorResponses = false, array $allowedServerRequestMethods = ['GET'] ): ?ResultInterface { + /** @var ?string $claimsParam */ $claimsParam = $request->getQueryParams()['claims'] ?? null; if ($claimsParam === null) { return null; } - $claims = json_decode($claimsParam, true); + /** @var ?array $claims */ + $claims = json_decode($claimsParam, true, 512, JSON_THROW_ON_ERROR); if (is_null($claims)) { return null; } @@ -60,7 +60,7 @@ public function checkRule( return new Result($this->getKey(), $claims); } - private function filterUnauthorizedClaims(array &$requestClaims, string $key, array $authorized) + private function filterUnauthorizedClaims(array &$requestClaims, string $key, array $authorized): void { if (!array_key_exists($key, $requestClaims)) { return; @@ -72,9 +72,7 @@ private function filterUnauthorizedClaims(array &$requestClaims, string $key, ar } $requestClaims[$key] = array_filter( $requested, - function ($key) use ($authorized) { - return in_array($key, $authorized); - }, + fn($key) => in_array($key, $authorized), ARRAY_FILTER_USE_KEY ); } diff --git a/src/Utils/Checker/Rules/RequiredNonceRule.php b/src/Utils/Checker/Rules/RequiredNonceRule.php index 6765c574..24f8f4ed 100644 --- a/src/Utils/Checker/Rules/RequiredNonceRule.php +++ b/src/Utils/Checker/Rules/RequiredNonceRule.php @@ -1,5 +1,7 @@ getOrFail(ScopeRule::class)->getValue(); - $isOpenIdScopePresent = (bool) array_filter($validScopes, function ($scopeEntity) { - return $scopeEntity->getIdentifier() === 'openid'; - }); + $isOpenIdScopePresent = (bool) array_filter( + $validScopes, + fn($scopeEntity) => $scopeEntity->getIdentifier() === 'openid' + ); if (! $isOpenIdScopePresent) { throw OidcServerException::invalidRequest( diff --git a/src/Utils/Checker/Rules/ResponseTypeRule.php b/src/Utils/Checker/Rules/ResponseTypeRule.php index f0c7211b..bdaabe0a 100644 --- a/src/Utils/Checker/Rules/ResponseTypeRule.php +++ b/src/Utils/Checker/Rules/ResponseTypeRule.php @@ -1,5 +1,7 @@ configurationService = $configurationService; - } - /** * @inheritDoc + * @throws Throwable */ public function checkRule( ServerRequestInterface $request, diff --git a/src/Utils/Checker/Rules/ScopeRule.php b/src/Utils/Checker/Rules/ScopeRule.php index 18c77230..f9960d72 100644 --- a/src/Utils/Checker/Rules/ScopeRule.php +++ b/src/Utils/Checker/Rules/ScopeRule.php @@ -1,5 +1,7 @@ scopeRepository = $scopeRepository; } /** @@ -43,7 +42,7 @@ public function checkRule( $scopeDelimiterString = $data['scope_delimiter_string'] ?? ' '; $scopes = $this->convertScopesQueryStringToArray( - $request->getQueryParams()['scope'] ?? $defaultScope, + (string)($request->getQueryParams()['scope'] ?? $defaultScope), $scopeDelimiterString ); @@ -65,9 +64,7 @@ public function checkRule( /** * Converts a scopes query string to an array to easily iterate for validation. * - * @param string $scopes - * @param string $scopeDelimiterString - * @return array + * @return string[] * @throws OidcServerException */ protected function convertScopesQueryStringToArray(string $scopes, string $scopeDelimiterString): array @@ -76,8 +73,6 @@ protected function convertScopesQueryStringToArray(string $scopes, string $scope throw OidcServerException::serverError('Scope delimiter string can not be empty.'); } - return array_filter(explode($scopeDelimiterString, trim($scopes)), function ($scope) { - return !empty($scope); - }); + return array_filter(explode($scopeDelimiterString, trim($scopes)), fn($scope) => !empty($scope)); } } diff --git a/src/Utils/Checker/Rules/StateRule.php b/src/Utils/Checker/Rules/StateRule.php index fa4b125a..b2647c1d 100644 --- a/src/Utils/Checker/Rules/StateRule.php +++ b/src/Utils/Checker/Rules/StateRule.php @@ -1,5 +1,7 @@ + * @copyright (c) 2018 Steve Rhoades + * @license http://opensource.org/licenses/MIT MIT */ -namespace SimpleSAML\Module\oidc; +namespace SimpleSAML\Module\oidc\Utils; use Lcobucci\JWT\Token\RegisteredClaims; -use OpenIDConnectServer\ClaimExtractor; -use OpenIDConnectServer\Entities\ClaimSetEntity; -use OpenIDConnectServer\Exception\InvalidArgumentException; +use League\OAuth2\Server\Entities\ScopeEntityInterface; use RuntimeException; +use SimpleSAML\Module\oidc\Entities\ClaimSetEntity; +use SimpleSAML\Module\oidc\Entities\Interfaces\ClaimSetEntityInterface; +use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException; -class ClaimTranslatorExtractor extends ClaimExtractor +class ClaimTranslatorExtractor { - /** @var array */ - protected $translationTable = [ + /** @var array */ + protected array $claimSets = []; + + /** @var string[] */ + protected array $protectedClaims = ['openid', 'profile', 'email', 'address', 'phone']; + + protected array $translationTable = [ 'sub' => [ 'eduPersonPrincipalName', 'eduPersonTargetedID', @@ -53,7 +67,7 @@ class ClaimTranslatorExtractor extends ClaimExtractor 'description', ], 'picture' => [ - // Empty 'jpegPhoto', Previously 'jpegPhoto' however spec calls for a url to photo, not an actual photo. + // Empty 'jpegPhoto', Previously 'jpegPhoto' however spec calls for an url to photo, not an actual photo. ], 'website' => [ // Empty @@ -99,7 +113,7 @@ class ClaimTranslatorExtractor extends ClaimExtractor /** * From JSON Web Token Claims registry: https://www.iana.org/assignments/jwt/jwt.xhtml */ - public const REGISTERED_CLAIMS = [ + final public const REGISTERED_CLAIMS = [ ...RegisteredClaims::ALL, 'azp', 'nonce', @@ -111,65 +125,132 @@ class ClaimTranslatorExtractor extends ClaimExtractor 'sub_jwk', ]; - /** - * Claims for which it is allowed to have multiple values. - */ - protected array $allowedMultiValueClaims; - /** * ClaimTranslatorExtractor constructor. * - * @param string $userIdAttr * @param ClaimSetEntity[] $claimSets - * @param array $translationTable - * @param array $allowedMultipleValueClaims - * @throws InvalidArgumentException + * @throws OidcServerException */ public function __construct( string $userIdAttr, array $claimSets = [], array $translationTable = [], - array $allowedMultipleValueClaims = [] + protected array $allowedMultiValueClaims = [] ) { // By default, add the userIdAttribute as one of the attribute for 'sub' claim. + /** @psalm-suppress MixedArgument */ array_unshift($this->translationTable['sub'], $userIdAttr); $this->translationTable = array_merge($this->translationTable, $translationTable); - $this->allowedMultiValueClaims = $allowedMultipleValueClaims; - - $this->protectedClaims[] = 'openid'; $this->addClaimSet(new ClaimSetEntity('openid', [ 'sub', ])); - parent::__construct($claimSets); + // Add Default OpenID Connect Claims + // @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + $this->addClaimSet( + new ClaimSetEntity('profile', [ + 'name', + 'family_name', + 'given_name', + 'middle_name', + 'nickname', + 'preferred_username', + 'profile', + 'picture', + 'website', + 'gender', + 'birthdate', + 'zoneinfo', + 'locale', + 'updated_at' + ]) + ); + $this->addClaimSet( + new ClaimSetEntity('email', [ + 'email', + 'email_verified' + ]) + ); + $this->addClaimSet( + new ClaimSetEntity('address', [ + 'address' + ]) + ); + $this->addClaimSet( + new ClaimSetEntity('phone', [ + 'phone_number', + 'phone_number_verified' + ]) + ); + + foreach ($claimSets as $claimSet) { + $this->addClaimSet($claimSet); + } } /** - * @param array $translationTable - * @param array $samlAttributes - * @return array + * @throws OidcServerException */ + public function addClaimSet(ClaimSetEntityInterface $claimSet): self + { + $scope = $claimSet->getScope(); + + if (in_array($scope, $this->protectedClaims) && !empty($this->claimSets[$scope])) { + throw OidcServerException::serverError( + sprintf("%s is a protected scope and is pre-defined by the OpenID Connect specification.", $scope) + ); + } + + $this->claimSets[$scope] = $claimSet; + + return $this; + } + + public function getClaimSet(string $scope): ?ClaimSetEntityInterface + { + if (!$this->hasClaimSet($scope)) { + return null; + } + + return $this->claimSets[$scope]; + } + + public function hasClaimSet(string $scope): bool + { + return array_key_exists($scope, $this->claimSets); + } + private function translateSamlAttributesToClaims(array $translationTable, array $samlAttributes): array { $claims = []; + /** + * @var string $claim + * @var array $mappingConfig + */ foreach ($translationTable as $claim => $mappingConfig) { - $type = $mappingConfig['type'] ?? 'string'; + $type = (string)($mappingConfig['type'] ?? 'string'); unset($mappingConfig['type']); if ($type === 'json') { - $subClaims = $this->translateSamlAttributesToClaims($mappingConfig['claims'], $samlAttributes); + $mappingConfigClaims = is_array($mappingConfig['claims']) ? $mappingConfig['claims'] : []; + $subClaims = $this->translateSamlAttributesToClaims($mappingConfigClaims, $samlAttributes); $claims[$claim] = $subClaims; continue; } // Look for attributes in the attribute key, if not set then assume to legacy style configuration - $attributes = $mappingConfig['attributes'] ?? $mappingConfig; + $attributes = isset($mappingConfig['attributes']) && is_array($mappingConfig['attributes']) ? + $mappingConfig['attributes'] : + $mappingConfig; + /** @var string $samlMatch */ foreach ($attributes as $samlMatch) { if (array_key_exists($samlMatch, $samlAttributes)) { + /** @psalm-suppress MixedAssignment, MixedArgument */ $values = in_array($claim, $this->allowedMultiValueClaims) ? - $samlAttributes[$samlMatch] : - current($samlAttributes[$samlMatch]); + $samlAttributes[$samlMatch] : + current($samlAttributes[$samlMatch]); + /** @psalm-suppress MixedAssignment */ $claims[$claim] = $this->convertType($type, $values); break; } @@ -178,11 +259,13 @@ private function translateSamlAttributesToClaims(array $translationTable, array return $claims; } - private function convertType(string $type, $attributes) + private function convertType(string $type, mixed $attributes): mixed { if (is_array($attributes)) { $values = []; + /** @psalm-suppress MixedAssignment */ foreach ($attributes as $attribute) { + /** @psalm-suppress MixedAssignment */ $values[] = $this->convertType($type, $attribute); } return $values; @@ -200,21 +283,52 @@ private function convertType(string $type, $attributes) return $attributes; } + /** + * @param array $scopes + */ public function extract(array $scopes, array $claims): array { - $translatedClaims = $this->translateSamlAttributesToClaims($this->translationTable, $claims); + $claims = $this->translateSamlAttributesToClaims($this->translationTable, $claims); + + $claimData = []; + $keys = array_keys($claims); + + foreach ($scopes as $scope) { + $scopeName = ($scope instanceof ScopeEntityInterface) ? $scope->getIdentifier() : $scope; + + $claimSet = $this->getClaimSet($scopeName); + if (null === $claimSet) { + continue; + } + + $intersected = array_intersect($claimSet->getClaims(), $keys); + + if (empty($intersected)) { + continue; + } + + $data = array_filter( + $claims, + fn($key) => in_array($key, $intersected), + ARRAY_FILTER_USE_KEY + ); + + $claimData = array_merge($claimData, $data); + } - return parent::extract($scopes, $translatedClaims); + return $claimData; } public function extractAdditionalIdTokenClaims(?array $claimsRequest, array $claims): array { + /** @var array $idTokenClaims */ $idTokenClaims = $claimsRequest['id_token'] ?? []; return $this->extractAdditionalClaims($idTokenClaims, $claims); } public function extractAdditionalUserInfoClaims(?array $claimsRequest, array $claims): array { + /** @var array $userInfoClaims */ $userInfoClaims = $claimsRequest['userinfo'] ?? []; return $this->extractAdditionalClaims($userInfoClaims, $claims); } @@ -223,8 +337,6 @@ public function extractAdditionalUserInfoClaims(?array $claimsRequest, array $cl * Add any individually requested claims * @link https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests * @param array $requestedClaims keys are requested claims, value is array of additional info on the request - * @param array $claims - * @return array */ private function extractAdditionalClaims(array $requestedClaims, array $claims): array { @@ -235,9 +347,7 @@ private function extractAdditionalClaims(array $requestedClaims, array $claims): return array_filter( $translatedClaims, - function ($key) use ($requestedClaims) { - return array_key_exists($key, $requestedClaims); - }, + fn(/** @param array-key $key */ $key) => array_key_exists($key, $requestedClaims), ARRAY_FILTER_USE_KEY ); } diff --git a/src/Utils/FingerprintGenerator.php b/src/Utils/FingerprintGenerator.php index cba68db0..a69093ac 100644 --- a/src/Utils/FingerprintGenerator.php +++ b/src/Utils/FingerprintGenerator.php @@ -1,7 +1,11 @@
- +
@@ -38,7 +41,10 @@
- + {{ '{oidc:client:is_confidential_help}'|trans }} diff --git a/templates/clients/delete.twig b/templates/clients/delete.twig index e0916615..05622129 100644 --- a/templates/clients/delete.twig +++ b/templates/clients/delete.twig @@ -21,12 +21,12 @@
- - + +
- -
diff --git a/templates/clients/index.twig b/templates/clients/index.twig index 810a44a5..f3b5cb6d 100644 --- a/templates/clients/index.twig +++ b/templates/clients/index.twig @@ -50,9 +50,15 @@
- - - + + + + + + + + +
@@ -70,13 +76,18 @@ diff --git a/templates/clients/show.twig b/templates/clients/show.twig index 9b6cc2b7..74878cf2 100644 --- a/templates/clients/show.twig +++ b/templates/clients/show.twig @@ -38,14 +38,18 @@ {{ '{oidc:client:identifier}'|trans }} {{ client.identifier }} - + {{ '{oidc:client:secret}'|trans }} {{ client.secret }} - + diff --git a/templates/install.twig b/templates/install.twig index ea272a5c..4f1cc44b 100644 --- a/templates/install.twig +++ b/templates/install.twig @@ -16,8 +16,8 @@ {% if oauth2_enabled %}
- - + +
{% endif %} diff --git a/tests/Form/ClientFormTest.php b/tests/Form/ClientFormTest.php deleted file mode 100644 index 419a632d..00000000 --- a/tests/Form/ClientFormTest.php +++ /dev/null @@ -1,17 +0,0 @@ -markTestIncomplete(); - } -} diff --git a/tests/Services/ConfigurationServiceTest.php b/tests/Services/ConfigurationServiceTest.php deleted file mode 100644 index 01adf0b1..00000000 --- a/tests/Services/ConfigurationServiceTest.php +++ /dev/null @@ -1,48 +0,0 @@ - $certDir - ] - ) - ); - Configuration::setPreLoadedConfig( - Configuration::loadFromArray([]), - 'module_oidc.php' - ); - // Test default cert and pem - $service = new ConfigurationService(); - $this->assertEquals($certDir . 'oidc_module.crt', $service->getCertPath()); - $this->assertEquals($certDir . 'oidc_module.key', $service->getPrivateKeyPath()); - - // Set customized - Configuration::setPreLoadedConfig( - Configuration::loadFromArray( - [ - 'privatekey' => 'myPrivateKey.key', - 'certificate' => 'myCertificate.crt', - ] - ), - 'module_oidc.php' - ); - $service = new ConfigurationService(); - $this->assertEquals($certDir . 'myCertificate.crt', $service->getCertPath()); - $this->assertEquals($certDir . 'myPrivateKey.key', $service->getPrivateKeyPath()); - } -} diff --git a/tests/Store/SessionLogoutTicketStoreBuilderTest.php b/tests/Store/SessionLogoutTicketStoreBuilderTest.php deleted file mode 100644 index 0eb62732..00000000 --- a/tests/Store/SessionLogoutTicketStoreBuilderTest.php +++ /dev/null @@ -1,32 +0,0 @@ - 'sqlite::memory:', - 'database.username' => null, - 'database.password' => null, - 'database.prefix' => 'phpunit_', - 'database.persistent' => true, - 'database.secondaries' => [], - ]; - - Configuration::loadFromArray($config, '', 'simplesaml'); - $builder = new SessionLogoutTicketStoreBuilder(); - - $this->assertInstanceOf(SessionLogoutTicketStoreInterface::class, $builder->getInstance()); - $this->assertInstanceOf(SessionLogoutTicketStoreInterface::class, $builder::getStaticInstance()); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 50e480f9..2a9a7344 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,7 @@ 'secret', - - // Tokens TTL - 'authCodeDuration' => 'PT10M', // 10 minutes - 'refreshTokenDuration' => 'P1M', // 1 month - 'accessTokenDuration' => 'PT1H', // 1 hour, + ModuleConfig::OPTION_TOKEN_AUTHORIZATION_CODE_TTL => 'PT10M', + ModuleConfig::OPTION_TOKEN_REFRESH_TOKEN_TTL => 'P1M', + ModuleConfig::OPTION_TOKEN_ACCESS_TOKEN_TTL => 'PT1H', - // Tag to run storage cleanup script using the cron module... - 'cron_tag' => 'hourly', + ModuleConfig::OPTION_CRON_TAG => 'hourly', - // Set token signer - // See Lcobucci\JWT\Signer algorithms in https://github.com/lcobucci/jwt/tree/master/src/Signer - 'signer' => \Lcobucci\JWT\Signer\Rsa\Sha256::class, - // 'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, - // 'signer' => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class, + ModuleConfig::OPTION_TOKEN_SIGNER => Sha256::class, - // this is the default auth source used for authentication if the auth source - // is not specified on particular client - 'auth' => 'default-sp', + ModuleConfig::OPTION_AUTH_SOURCE => 'default-sp', - // useridattr is the attribute-name that contains the userid as returned from idp. By default, this attribute - // will be dynamically added to the 'sub' claim in the attribute-to-claim translation table (you will probably - // want to use this attribute as the 'sub' claim since it designates unique identifier for the user). - 'useridattr' => 'uid', + ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE => 'uid', - // Optional custom scopes. You can create as many scopes as you want and assign claims to them. - 'scopes' => [ -// 'private' => [ // The key represents the scope name. -// 'description' => 'private scope', -// 'claim_name_prefix' => '', // Prefix to apply for all claim names from this scope -// 'are_multiple_claim_values_allowed' => false, // Are claims for this scope allowed to have multiple values -// 'claims' => ['national_document_id'] // Claims from the translation table which this scope will contain -// ], + ModuleConfig::OPTION_AUTH_CUSTOM_SCOPES => [ ], - 'translate' => [ - /* - * This is the default translate table from SAML to OIDC. - * You can change here the behaviour or add more translation to your - * private attributes scopes - * - * The basic format is - * - * 'claimName' => [ - * 'type' => 'string|int|bool|json', - * // For non JSON types - * 'attributes' => ['samlAttribute1', 'samlAttribute2'] - * // For JSON types - * 'claims => [ - * 'subclaim' => [ 'type' => 'string', 'attributes' => ['saml1']] - * ] - * ] - * - * For convenience the default type is "string" so type does not need to be defined. - * If "attributes" is not set, then it is assumed that the rest of the values are saml - * attribute names. - * - * Note on 'sub' claim: by default, the list of attributes for 'sub' claim will also contain attribute defined - * in 'useridattr' setting. You will probably want to use this attribute as the 'sub' claim since it - * designates unique identifier for the user, However, override as necessary. - */ -// 'sub' => [ -// 'attribute-defined-in-useridattr', // will be dynamically added if the list for 'sub' claim is not set. -// 'eduPersonPrincipalName', -// 'eduPersonTargetedID', -// 'eduPersonUniqueId', -// ], -// 'name' => [ -// 'cn', -// 'displayName', -// ], -// 'family_name' => [ -// 'sn', -// ], -// 'given_name' => [ -// 'givenName', -// ], -// 'middle_name' => [ -// // Empty -// ], -// 'nickname' => [ -// 'eduPersonNickname', -// ], -// 'preferred_username' => [ -// 'uid', -// ], -// 'profile' => [ -// 'labeledURI', -// 'description', -// ], -// 'picture' => [ -// // Empty. Previously 'jpegPhoto' however spec calls for a url to photo, not an actual photo. -// ], -// 'website' => [ -// // Empty -// ], -// 'gender' => [ -// // Empty -// ], -// 'birthdate' => [ -// // Empty -// ], -// 'zoneinfo' => [ -// // Empty -// ], -// 'locale' => [ -// 'preferredLanguage', -// ], -// 'updated_at' => [ -// 'type' => 'int', -// 'attributes' => [], -// ], -// 'email' => [ -// 'mail', -// ], -// 'email_verified' => [ -// 'type' => 'bool', -// 'attributes' => [], -// ], -// // address is a json object. Set the 'formatted' sub claim to postalAddress -// 'address' => [ -// 'type' => 'json', -// 'claims' => [ -// 'formatted' => ['postalAddress'], -// ] -// ], -// 'phone_number' => [ -// 'mobile', -// 'telephoneNumber', -// 'homePhone', -// ], -// 'phone_number_verified' => [ -// 'type' => 'bool', -// 'attributes' => [], -// ], - /* - * Optional scopes attributes - */ -// 'national_document_id' => [ -// 'schacPersonalUniqueId', -// ], + ModuleConfig::OPTION_AUTH_SAML_TO_OIDC_TRANSLATE_TABLE => [ ], - // Optional list of the Authentication Context Class References that this OP supports. - // If populated, this list will be available in OP discovery document (OP Metadata) as 'acr_values_supported'. - // @see https://datatracker.ietf.org/doc/html/rfc6711 - // @see https://www.iana.org/assignments/loa-profiles/loa-profiles.xhtml - // @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken (acr claim) - // @see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest (acr_values parameter) - // Syntax: string[] (array of strings) - 'acrValuesSupported' => [ -// 'https://refeds.org/assurance/profile/espresso', -// 'https://refeds.org/assurance/profile/cappuccino', -// 'https://refeds.org/profile/mfa', -// 'https://refeds.org/profile/sfa', -// 'urn:mace:incommon:iap:silver', -// 'urn:mace:incommon:iap:bronze', -// '4', -// '3', -// '2', -// '1', -// '0', -// '...', + ModuleConfig::OPTION_AUTH_ACR_VALUES_SUPPORTED => [ ], - // If this OP supports ACRs, indicate which usable auth source supports which ACRs. - // Order of ACRs is important, more important ones being first. - // Syntax: array (array with auth source as key and value being array of ACR values as strings) - 'authSourcesToAcrValuesMap' => [ -// 'example-userpass' => ['1', '0'], -// 'default-sp' => ['http://id.incommon.org/assurance/bronze', '2', '1', '0'], -// 'strongly-assured-authsource' => [ -// 'https://refeds.org/assurance/profile/espresso', -// 'https://refeds.org/profile/mfa', -// 'https://refeds.org/assurance/profile/cappuccino', -// 'https://refeds.org/profile/sfa', -// '3', -// '2', -// '1', -// '0', -// ], + ModuleConfig::OPTION_AUTH_SOURCES_TO_ACR_VALUES_MAP => [ ], - // If this OP supports ACRs, indicate if authentication using cookie should be forced to specific ACR value. - // If this option is set to null, no specific ACR will be forced for cookie authentication and the resulting ACR - // will be one of the ACRs supported on used auth source during authentication, that is, session creation. - // If this option is set to specific ACR, with ACR value being one of the ACR value this OP supports, it will be - // set to that ACR for cookie authentication. - // For example, OIDC Core Spec notes that authentication using a long-lived browser cookie is one example where - // the use of "level 0" is appropriate: -// 'forcedAcrValueForCookieAuthentication' => '0', - 'forcedAcrValueForCookieAuthentication' => null, + ModuleConfig::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION => null, ]; diff --git a/tests/Controller/OAuth2AccessTokenControllerTest.php b/tests/src/Controller/AccessTokenControllerTest.php similarity index 57% rename from tests/Controller/OAuth2AccessTokenControllerTest.php rename to tests/src/Controller/AccessTokenControllerTest.php index 9f9a3908..d171add5 100644 --- a/tests/Controller/OAuth2AccessTokenControllerTest.php +++ b/tests/src/Controller/AccessTokenControllerTest.php @@ -1,31 +1,30 @@ authorizationServerMock = $this->createMock(AuthorizationServer::class); @@ -37,11 +36,14 @@ protected function setUp(): void public function testItIsInitializable(): void { $this->assertInstanceOf( - OAuth2AccessTokenController::class, - new OAuth2AccessTokenController($this->authorizationServerMock) + AccessTokenController::class, + new AccessTokenController($this->authorizationServerMock) ); } + /** + * @throws OAuthServerException + */ public function testItRespondsToAccessTokenRequest(): void { $this->authorizationServerMock @@ -52,7 +54,7 @@ public function testItRespondsToAccessTokenRequest(): void $this->assertSame( $this->responseMock, - (new OAuth2AccessTokenController($this->authorizationServerMock))->__invoke($this->serverRequestMock) + (new AccessTokenController($this->authorizationServerMock))->__invoke($this->serverRequestMock) ); } } diff --git a/tests/Controller/OAuth2AuthorizationControllerTest.php b/tests/src/Controller/AuthorizationControllerTest.php similarity index 84% rename from tests/Controller/OAuth2AuthorizationControllerTest.php rename to tests/src/Controller/AuthorizationControllerTest.php index 73ff698d..bfb3580e 100644 --- a/tests/Controller/OAuth2AuthorizationControllerTest.php +++ b/tests/src/Controller/AuthorizationControllerTest.php @@ -1,58 +1,40 @@ ['1', '0'], 'essential' => false]; + /** + * @throws Exception + */ public function setUp(): void { $this->authenticationServiceStub = $this->createStub(AuthenticationService::class); $this->authorizationServerStub = $this->createStub(AuthorizationServer::class); - $this->configurationServiceStub = $this->createStub(ConfigurationService::class); + $this->moduleConfigStub = $this->createStub(ModuleConfig::class); $this->loggerServiceMock = $this->createMock(LoggerService::class); $this->authorizationRequestMock = $this->createMock(AuthorizationRequest::class); @@ -79,6 +64,7 @@ public function setUp(): void * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testReturnsResponseWhenInvoked(): void { @@ -91,10 +77,10 @@ public function testReturnsResponseWhenInvoked(): void $this->authenticationServiceStub->method('getAuthenticateUser')->willReturn($this->userEntityStub); - $controller = new OAuth2AuthorizationController( + $controller = new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ); @@ -107,6 +93,7 @@ public function testReturnsResponseWhenInvoked(): void * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testValidateAcrThrowsIfAuthSourceIdNotSetInAuthorizationRequest(): void { @@ -120,10 +107,10 @@ public function testValidateAcrThrowsIfAuthSourceIdNotSetInAuthorizationRequest( $this->expectException(OidcServerException::class); - (new OAuth2AuthorizationController( + (new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ))($this->serverRequestStub); } @@ -134,6 +121,7 @@ public function testValidateAcrThrowsIfAuthSourceIdNotSetInAuthorizationRequest( * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testValidateAcrThrowsIfCookieBasedAuthnNotSetInAuthorizationRequest(): void { @@ -149,10 +137,10 @@ public function testValidateAcrThrowsIfCookieBasedAuthnNotSetInAuthorizationRequ $this->expectException(OidcServerException::class); - (new OAuth2AuthorizationController( + (new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ))($this->serverRequestStub); } @@ -163,6 +151,7 @@ public function testValidateAcrThrowsIfCookieBasedAuthnNotSetInAuthorizationRequ * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testValidateAcrSetsForcedAcrForCookieAuthentication(): void { @@ -173,10 +162,10 @@ public function testValidateAcrSetsForcedAcrForCookieAuthentication(): void $this->authorizationRequestMock->method('getAuthSourceId')->willReturn(self::$sampleAuthSourceId); $this->authorizationRequestMock->method('getIsCookieBasedAuthn')->willReturn(true); - $this->configurationServiceStub + $this->moduleConfigStub ->method('getAuthSourcesToAcrValuesMap') ->willReturn(self::$sampleAuthSourcesToAcrValuesMap); - $this->configurationServiceStub->method('getForcedAcrValueForCookieAuthentication')->willReturn('0'); + $this->moduleConfigStub->method('getForcedAcrValueForCookieAuthentication')->willReturn('0'); $this->authorizationServerStub ->method('validateAuthorizationRequest') @@ -187,10 +176,10 @@ public function testValidateAcrSetsForcedAcrForCookieAuthentication(): void $this->authorizationRequestMock->expects($this->once())->method('setAcr')->with('0'); - (new OAuth2AuthorizationController( + (new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ))($this->serverRequestStub); } @@ -201,6 +190,7 @@ public function testValidateAcrSetsForcedAcrForCookieAuthentication(): void * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testValidateAcrThrowsIfNoMatchedAcrForEssentialAcrs(): void { @@ -212,7 +202,7 @@ public function testValidateAcrThrowsIfNoMatchedAcrForEssentialAcrs(): void $this->authorizationRequestMock->method('getAuthSourceId')->willReturn(self::$sampleAuthSourceId); $this->authorizationRequestMock->method('getIsCookieBasedAuthn')->willReturn(false); - $this->configurationServiceStub + $this->moduleConfigStub ->method('getAuthSourcesToAcrValuesMap') ->willReturn(self::$sampleAuthSourcesToAcrValuesMap); @@ -225,10 +215,10 @@ public function testValidateAcrThrowsIfNoMatchedAcrForEssentialAcrs(): void $this->expectException(OidcServerException::class); - (new OAuth2AuthorizationController( + (new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ))($this->serverRequestStub); } @@ -239,6 +229,7 @@ public function testValidateAcrThrowsIfNoMatchedAcrForEssentialAcrs(): void * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testValidateAcrSetsFirstMatchedAcr(): void { @@ -249,7 +240,7 @@ public function testValidateAcrSetsFirstMatchedAcr(): void $this->authorizationRequestMock->method('getAuthSourceId')->willReturn(self::$sampleAuthSourceId); $this->authorizationRequestMock->method('getIsCookieBasedAuthn')->willReturn(false); - $this->configurationServiceStub + $this->moduleConfigStub ->method('getAuthSourcesToAcrValuesMap') ->willReturn(self::$sampleAuthSourcesToAcrValuesMap); @@ -262,10 +253,10 @@ public function testValidateAcrSetsFirstMatchedAcr(): void $this->authorizationRequestMock->expects($this->once())->method('setAcr')->with('1'); - (new OAuth2AuthorizationController( + (new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ))($this->serverRequestStub); } @@ -276,6 +267,7 @@ public function testValidateAcrSetsFirstMatchedAcr(): void * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testValidateAcrSetsCurrentSessionAcrIfNoMatchedAcr(): void { @@ -287,7 +279,7 @@ public function testValidateAcrSetsCurrentSessionAcrIfNoMatchedAcr(): void $this->authorizationRequestMock->method('getAuthSourceId')->willReturn(self::$sampleAuthSourceId); $this->authorizationRequestMock->method('getIsCookieBasedAuthn')->willReturn(false); - $this->configurationServiceStub + $this->moduleConfigStub ->method('getAuthSourcesToAcrValuesMap') ->willReturn(self::$sampleAuthSourcesToAcrValuesMap); @@ -300,10 +292,10 @@ public function testValidateAcrSetsCurrentSessionAcrIfNoMatchedAcr(): void $this->authorizationRequestMock->expects($this->once())->method('setAcr')->with('1'); - (new OAuth2AuthorizationController( + (new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ))($this->serverRequestStub); } @@ -314,6 +306,7 @@ public function testValidateAcrSetsCurrentSessionAcrIfNoMatchedAcr(): void * @throws Error\NotFound * @throws Error\Exception * @throws OAuthServerException + * @throws Throwable */ public function testValidateAcrLogsWarningIfNoAcrsConfigured(): void { @@ -325,7 +318,7 @@ public function testValidateAcrLogsWarningIfNoAcrsConfigured(): void $this->authorizationRequestMock->method('getIsCookieBasedAuthn')->willReturn(false); $authSourcesToAcrValuesMap = [self::$sampleAuthSourceId => []]; - $this->configurationServiceStub + $this->moduleConfigStub ->method('getAuthSourcesToAcrValuesMap') ->willReturn($authSourcesToAcrValuesMap); @@ -339,10 +332,10 @@ public function testValidateAcrLogsWarningIfNoAcrsConfigured(): void $this->authorizationRequestMock->expects($this->once())->method('setAcr'); $this->loggerServiceMock->expects($this->once())->method('warning'); - (new OAuth2AuthorizationController( + (new AuthorizationController( $this->authenticationServiceStub, $this->authorizationServerStub, - $this->configurationServiceStub, + $this->moduleConfigStub, $this->loggerServiceMock ))($this->serverRequestStub); } diff --git a/tests/Controller/ClientCreateControllerTest.php b/tests/src/Controller/Client/CreateControllerTest.php similarity index 79% rename from tests/Controller/ClientCreateControllerTest.php rename to tests/src/Controller/Client/CreateControllerTest.php index e18d0fe6..9dcf9c4b 100644 --- a/tests/Controller/ClientCreateControllerTest.php +++ b/tests/src/Controller/Client/CreateControllerTest.php @@ -1,15 +1,20 @@ clientRepositoryMock = $this->createMock(ClientRepository::class); @@ -75,12 +56,12 @@ protected function setUp(): void public function testCanInstantiate(): void { $controller = $this->getStubbedInstance(); - $this->assertInstanceOf(ClientCreateController::class, $controller); + $this->assertInstanceOf(CreateController::class, $controller); } - protected function getStubbedInstance(): ClientCreateController + protected function getStubbedInstance(): CreateController { - return new ClientCreateController( + return new \SimpleSAML\Module\oidc\Controller\Client\CreateController( $this->clientRepositoryMock, $this->allowedOriginRepositoryMock, $this->templateFactoryMock, @@ -90,6 +71,9 @@ protected function getStubbedInstance(): ClientCreateController ); } + /** + * @throws Exception + */ public function testCanShowNewClientForm(): void { $this->clientFormMock @@ -119,9 +103,12 @@ public function testCanShowNewClientForm(): void ->willReturn($this->clientFormMock); $controller = $this->getStubbedInstance(); - $this->assertSame($this->templateStub, $controller->__invoke($this->serverRequestStub)); + $this->assertSame($this->templateStub, $controller->__invoke()); } + /** + * @throws Exception + */ public function testCanCreateNewClientFromFormData(): void { $this->clientFormMock @@ -170,9 +157,12 @@ public function testCanCreateNewClientFromFormData(): void ->with('{oidc:client:added}'); $controller = $this->getStubbedInstance(); - $this->assertInstanceOf(RedirectResponse::class, $controller->__invoke($this->serverRequestStub)); + $this->assertInstanceOf(RedirectResponse::class, $controller->__invoke()); } + /** + * @throws Exception + */ public function testCanSetOwnerInNewClient(): void { $this->authContextServiceMock->expects($this->once())->method('isSspAdmin')->willReturn(false); @@ -211,10 +201,8 @@ public function testCanSetOwnerInNewClient(): void ->willReturn($this->clientFormMock); $this->clientRepositoryMock->expects($this->once())->method('add') - ->with($this->callback(function ($client) { - return is_callable([$client, 'getOwner']) && - $client->getOwner() == 'ownerUsername'; - })); + ->with($this->callback(fn($client) => is_callable([$client, 'getOwner']) && + $client->getOwner() == 'ownerUsername')); $this->sessionMessageServiceMock ->expects($this->once()) @@ -222,6 +210,6 @@ public function testCanSetOwnerInNewClient(): void ->with('{oidc:client:added}'); $controller = $this->getStubbedInstance(); - $this->assertInstanceOf(RedirectResponse::class, $controller->__invoke($this->serverRequestStub)); + $this->assertInstanceOf(RedirectResponse::class, $controller->__invoke()); } } diff --git a/tests/Controller/ClientDeleteControllerTest.php b/tests/src/Controller/Client/DeleteControllerTest.php similarity index 77% rename from tests/Controller/ClientDeleteControllerTest.php rename to tests/src/Controller/Client/DeleteControllerTest.php index 28d04465..b10ab8ec 100644 --- a/tests/Controller/ClientDeleteControllerTest.php +++ b/tests/src/Controller/Client/DeleteControllerTest.php @@ -1,58 +1,46 @@ clientRepositoryMock = $this->createMock(ClientRepository::class); @@ -66,9 +54,9 @@ protected function setUp(): void $this->templateStub = $this->createStub(Template::class); } - protected function getStubbedInstance(): ClientDeleteController + protected function getStubbedInstance(): \SimpleSAML\Module\oidc\Controller\Client\DeleteController { - return new ClientDeleteController( + return new DeleteController( $this->clientRepositoryMock, $this->templateFactoryMock, $this->sessionMessageServiceMock, @@ -79,9 +67,12 @@ protected function getStubbedInstance(): ClientDeleteController public function testCanInstantiate(): void { $controller = $this->getStubbedInstance(); - $this->assertInstanceOf(ClientDeleteController::class, $controller); + $this->assertInstanceOf(\SimpleSAML\Module\oidc\Controller\Client\DeleteController::class, $controller); } + /** + * @throws ConfigurationError|BadRequest|NotFound|\SimpleSAML\Error\Exception|OidcServerException|JsonException + */ public function testItAsksConfirmationBeforeDeletingClient(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams') @@ -99,6 +90,9 @@ public function testItAsksConfirmationBeforeDeletingClient(): void $this->assertInstanceOf(Template::class, $controller->__invoke($this->serverRequestMock)); } + /** + * @throws ConfigurationError|BadRequest|NotFound|\SimpleSAML\Error\Exception|OidcServerException|JsonException + */ public function testThrowsIfIdNotFoundInDeleteAction(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams')->willReturn([]); @@ -108,6 +102,9 @@ public function testThrowsIfIdNotFoundInDeleteAction(): void ($this->getStubbedInstance())->__invoke($this->serverRequestMock); } + /** + * @throws ConfigurationError|BadRequest|NotFound|\SimpleSAML\Error\Exception|OidcServerException|JsonException + */ public function testThrowsIfSecretNotFoundInDeleteAction(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams') @@ -122,6 +119,9 @@ public function testThrowsIfSecretNotFoundInDeleteAction(): void ($this->getStubbedInstance())->__invoke($this->serverRequestMock); } + /** + * @throws ConfigurationError|BadRequest|NotFound|\SimpleSAML\Error\Exception|OidcServerException|JsonException + */ public function testThrowsIfSecretIsInvalidInDeleteAction(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams') @@ -138,6 +138,9 @@ public function testThrowsIfSecretIsInvalidInDeleteAction(): void ($this->getStubbedInstance())->__invoke($this->serverRequestMock); } + /** + * @throws ConfigurationError|BadRequest|NotFound|\SimpleSAML\Error\Exception|OidcServerException|JsonException + */ public function testItDeletesClient(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams') @@ -159,6 +162,9 @@ public function testItDeletesClient(): void ); } + /** + * @throws ConfigurationError|BadRequest|NotFound|\SimpleSAML\Error\Exception|OidcServerException|JsonException + */ public function testItDeletesClientWithOwner(): void { $this->authContextServiceMock->expects($this->exactly(2))->method('isSspAdmin')->willReturn(false); diff --git a/tests/Controller/ClientEditControllerTest.php b/tests/src/Controller/Client/EditControllerTest.php similarity index 85% rename from tests/Controller/ClientEditControllerTest.php rename to tests/src/Controller/Client/EditControllerTest.php index 8848dd58..df6bff3c 100644 --- a/tests/Controller/ClientEditControllerTest.php +++ b/tests/src/Controller/Client/EditControllerTest.php @@ -1,82 +1,54 @@ configurationServiceMock = $this->createMock(ConfigurationService::class); + $this->moduleConfigMock = $this->createMock(ModuleConfig::class); $this->clientRepositoryMock = $this->createMock(ClientRepository::class); $this->allowedOriginRepositoryMock = $this->createMock(AllowedOriginRepository::class); $this->templateFactoryMock = $this->createMock(TemplateFactory::class); @@ -90,16 +62,15 @@ protected function setUp(): void $this->templateStub = $this->createStub(Template::class); $this->clientFormMock = $this->createMock(ClientForm::class); - $this->configurationServiceMock->method('getOpenIdConnectModuleURL')->willReturn('url'); + $this->moduleConfigMock->method('getOpenIdConnectModuleURL')->willReturn('url'); $this->uriStub->method('getPath')->willReturn('/'); $this->serverRequestMock->method('getUri')->willReturn($this->uriStub); $this->serverRequestMock->method('withQueryParams')->willReturn($this->serverRequestMock); } - protected function getStubbedInstance(): ClientEditController + protected function getStubbedInstance(): EditController { - return new ClientEditController( - $this->configurationServiceMock, + return new EditController( $this->clientRepositoryMock, $this->allowedOriginRepositoryMock, $this->templateFactoryMock, @@ -111,9 +82,17 @@ protected function getStubbedInstance(): ClientEditController public function testItIsInitializable(): void { - $this->assertInstanceOf(ClientEditController::class, $this->getStubbedInstance()); + $this->assertInstanceOf( + EditController::class, + $this->getStubbedInstance() + ); } + /** + * @throws BadRequest + * @throws \SimpleSAML\Error\Exception + * @throws NotFound + */ public function testItShowsEditClientForm(): void { $this->authContextServiceMock->method('isSspAdmin')->willReturn(true); @@ -160,6 +139,11 @@ public function testItShowsEditClientForm(): void ); } + /** + * @throws BadRequest + * @throws \SimpleSAML\Error\Exception + * @throws NotFound + */ public function testItUpdatesClientFromEditClientFormData(): void { $this->authContextServiceMock->method('isSspAdmin')->willReturn(true); @@ -227,7 +211,6 @@ public function testItUpdatesClientFromEditClientFormData(): void false, 'auth_source', 'existingOwner', - [] ), null ); @@ -242,6 +225,11 @@ public function testItUpdatesClientFromEditClientFormData(): void ); } + /** + * @throws BadRequest + * @throws \SimpleSAML\Error\Exception + * @throws NotFound + */ public function testItSendsOwnerArgToRepoOnUpdate(): void { $this->authContextServiceMock->expects($this->atLeastOnce())->method('isSspAdmin')->willReturn(false); @@ -309,8 +297,6 @@ public function testItSendsOwnerArgToRepoOnUpdate(): void false, 'auth_source', 'existingOwner', - [], - null ), 'authedUserId' ); @@ -327,6 +313,10 @@ public function testItSendsOwnerArgToRepoOnUpdate(): void ); } + /** + * @throws \SimpleSAML\Error\Exception + * @throws NotFound + */ public function testThrowsIdNotFoundExceptionInEditAction(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams')->willReturn([]); @@ -336,13 +326,18 @@ public function testThrowsIdNotFoundExceptionInEditAction(): void ($this->getStubbedInstance())->__invoke($this->serverRequestMock); } + /** + * @throws BadRequest + * @throws \SimpleSAML\Error\Exception + * @throws NotFound + */ public function testThrowsClientNotFoundExceptionInEditAction(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams') ->willReturn(['client_id' => 'clientid']); $this->clientRepositoryMock->expects($this->once())->method('findById')->willReturn(null); - $this->expectException(\Exception::class); + $this->expectException(Exception::class); ($this->getStubbedInstance())->__invoke($this->serverRequestMock); } diff --git a/tests/Controller/ClientIndexControllerTest.php b/tests/src/Controller/Client/IndexControllerTest.php similarity index 62% rename from tests/Controller/ClientIndexControllerTest.php rename to tests/src/Controller/Client/IndexControllerTest.php index 3385e907..addfa32c 100644 --- a/tests/Controller/ClientIndexControllerTest.php +++ b/tests/src/Controller/Client/IndexControllerTest.php @@ -1,52 +1,36 @@ clientRepositoryMock = $this->createMock(ClientRepository::class); @@ -63,9 +47,9 @@ protected function setUp(): void $this->serverRequestMock->method('getQueryParams')->willReturn(['page' => 1]); } - protected function getStubbedInstance(): ClientIndexController + protected function getStubbedInstance(): IndexController { - return new ClientIndexController( + return new IndexController( $this->clientRepositoryMock, $this->templateFactoryMock, $this->authContextServiceMock @@ -74,9 +58,12 @@ protected function getStubbedInstance(): ClientIndexController public function testItIsInitializable(): void { - $this->assertInstanceOf(ClientIndexController::class, $this->getStubbedInstance()); + $this->assertInstanceOf(IndexController::class, $this->getStubbedInstance()); } + /** + * @throws \SimpleSAML\Error\Exception + */ public function testItShowsClientIndex(): void { $this->clientRepositoryMock->expects($this->once())->method('findPaginated') diff --git a/tests/Controller/ClientResetSecretControllerTest.php b/tests/src/Controller/Client/ResetSecretControllerTest.php similarity index 82% rename from tests/Controller/ClientResetSecretControllerTest.php rename to tests/src/Controller/Client/ResetSecretControllerTest.php index 3eff31a1..f83d6414 100644 --- a/tests/Controller/ClientResetSecretControllerTest.php +++ b/tests/src/Controller/Client/ResetSecretControllerTest.php @@ -1,46 +1,38 @@ clientRepositoryMock = $this->createMock(ClientRepository::class); @@ -58,9 +50,9 @@ public static function setUpBeforeClass(): void $_SERVER['REQUEST_URI'] = ''; } - protected function prepareStubbedInstance(): ClientResetSecretController + protected function prepareStubbedInstance(): \SimpleSAML\Module\oidc\Controller\Client\ResetSecretController { - return new ClientResetSecretController( + return new ResetSecretController( $this->clientRepositoryMock, $this->sessionMessagesServiceMock, $this->authContextServiceMock @@ -70,11 +62,15 @@ protected function prepareStubbedInstance(): ClientResetSecretController public function testCanInstantiate(): void { $this->assertInstanceOf( - ClientResetSecretController::class, + \SimpleSAML\Module\oidc\Controller\Client\ResetSecretController::class, $this->prepareStubbedInstance() ); } + /** + * @throws Exception + * @throws NotFound + */ public function testItThrowsIdNotFoundExceptionInResetSecretAction(): void { $this->serverRequestMock->method('getQueryParams')->willReturn([]); @@ -82,6 +78,10 @@ public function testItThrowsIdNotFoundExceptionInResetSecretAction(): void $this->prepareStubbedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws BadRequest + * @throws Exception + */ public function testItThrowsClientNotFoundExceptionInResetSecretAction(): void { $this->serverRequestMock->method('getQueryParams')->willReturn(['client_id' => 'clientid']); @@ -95,6 +95,10 @@ public function testItThrowsClientNotFoundExceptionInResetSecretAction(): void $this->prepareStubbedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws Exception + * @throws NotFound + */ public function testThrowsSecretNotFoundExceptionInResetSecretAction(): void { $this->serverRequestMock @@ -112,6 +116,10 @@ public function testThrowsSecretNotFoundExceptionInResetSecretAction(): void $this->prepareStubbedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws Exception + * @throws NotFound + */ public function testThrowsSecretInvalidExceptionInResetSecretAction(): void { $this->serverRequestMock @@ -135,6 +143,11 @@ public function testThrowsSecretInvalidExceptionInResetSecretAction(): void $this->prepareStubbedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws BadRequest + * @throws Exception + * @throws NotFound + */ public function testItResetSecretsClient(): void { $this->serverRequestMock @@ -169,6 +182,11 @@ public function testItResetSecretsClient(): void $this->prepareStubbedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws BadRequest + * @throws Exception + * @throws NotFound + */ public function testItSendBackToShowClientIfNotPostMethodInResetAction(): void { $this->serverRequestMock diff --git a/tests/Controller/ClientShowControllerTest.php b/tests/src/Controller/Client/ShowControllerTest.php similarity index 71% rename from tests/Controller/ClientShowControllerTest.php rename to tests/src/Controller/Client/ShowControllerTest.php index c0bef3cd..f3dc4a11 100644 --- a/tests/Controller/ClientShowControllerTest.php +++ b/tests/src/Controller/Client/ShowControllerTest.php @@ -1,55 +1,43 @@ clientRepositoryMock = $this->createMock(ClientRepository::class); @@ -69,9 +57,9 @@ public static function setUpBeforeClass(): void $_SERVER['REQUEST_URI'] = ''; } - protected function getStubbedInstance(): ClientShowController + protected function getStubbedInstance(): ShowController { - return new ClientShowController( + return new ShowController( $this->clientRepositoryMock, $this->allowedOriginRepositoryMock, $this->templateFactoryMock, @@ -81,9 +69,18 @@ protected function getStubbedInstance(): ClientShowController public function testItIsInitializable(): void { - $this->assertInstanceOf(ClientShowController::class, $this->getStubbedInstance()); + $this->assertInstanceOf( + ShowController::class, + $this->getStubbedInstance() + ); } + /** + * @throws BadRequest + * @throws \SimpleSAML\Error\Exception + * @throws NotFound + * @throws OidcServerException|JsonException + */ public function testItShowsClientDescription(): void { $this->serverRequestMock @@ -117,6 +114,11 @@ public function testItShowsClientDescription(): void ); } + /** + * @throws \SimpleSAML\Error\Exception + * @throws NotFound + * @throws OidcServerException|JsonException + */ public function testItThrowsIdNotFoundException(): void { $this->serverRequestMock->expects($this->once())->method('getQueryParams')->willReturn([]); @@ -125,6 +127,11 @@ public function testItThrowsIdNotFoundException(): void $this->getStubbedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws BadRequest + * @throws \SimpleSAML\Error\Exception + * @throws OidcServerException|JsonException + */ public function testItThrowsClientNotFoundException(): void { $this->serverRequestMock diff --git a/tests/Controller/OpenIdConnectDiscoverConfigurationControllerTest.php b/tests/src/Controller/ConfigurationDiscoveryControllerTest.php similarity index 56% rename from tests/Controller/OpenIdConnectDiscoverConfigurationControllerTest.php rename to tests/src/Controller/ConfigurationDiscoveryControllerTest.php index 37945b4b..75bbd30c 100644 --- a/tests/Controller/OpenIdConnectDiscoverConfigurationControllerTest.php +++ b/tests/src/Controller/ConfigurationDiscoveryControllerTest.php @@ -1,18 +1,22 @@ 'http://localhost', 'authorization_endpoint' => 'http://localhost/authorize.php', 'token_endpoint' => 'http://localhost/token.php', @@ -25,18 +29,16 @@ class OpenIdConnectDiscoverConfigurationControllerTest extends TestCase 'code_challenge_methods_supported' => ['plain', 'S256'], 'end_session_endpoint' => 'http://localhost/logout.php', ]; + + protected MockObject $oidcOpenIdProviderMetadataServiceMock; + protected MockObject $serverRequestMock; + /** - * \PHPUnit\Framework\MockObject\MockObject - */ - protected $oidcOpenIdProviderMetadataServiceMock; - /** - * \PHPUnit\Framework\MockObject\MockObject + * @throws Exception */ - protected $serverRequestMock; - protected function setUp(): void { - $this->oidcOpenIdProviderMetadataServiceMock = $this->createMock(OidcOpenIdProviderMetadataService::class); + $this->oidcOpenIdProviderMetadataServiceMock = $this->createMock(OpMetadataService::class); $this->oidcOpenIdProviderMetadataServiceMock->method('getMetadata')->willReturn(self::OIDC_OP_METADATA); $this->serverRequestMock = $this->createMock(ServerRequest::class); @@ -45,20 +47,20 @@ protected function setUp(): void public function testItIsInitializable(): void { $this->assertInstanceOf( - OpenIdConnectDiscoverConfigurationController::class, - new OpenIdConnectDiscoverConfigurationController($this->oidcOpenIdProviderMetadataServiceMock) + ConfigurationDiscoveryController::class, + new ConfigurationDiscoveryController($this->oidcOpenIdProviderMetadataServiceMock) ); } - protected function getStubbedInstance(): OpenIdConnectDiscoverConfigurationController + protected function getStubbedInstance(): ConfigurationDiscoveryController { - return new OpenIdConnectDiscoverConfigurationController($this->oidcOpenIdProviderMetadataServiceMock); + return new ConfigurationDiscoveryController($this->oidcOpenIdProviderMetadataServiceMock); } public function testItReturnsOpenIdConnectConfiguration(): void { $this->assertSame( - $this->getStubbedInstance()->__invoke($this->serverRequestMock)->getPayload(), + json_decode($this->getStubbedInstance()->__invoke()->getContent(), true), self::OIDC_OP_METADATA ); } diff --git a/tests/Controller/OpenIdConnectInstallerControllerTest.php b/tests/src/Controller/InstallerControllerTest.php similarity index 80% rename from tests/Controller/OpenIdConnectInstallerControllerTest.php rename to tests/src/Controller/InstallerControllerTest.php index d9925c5d..0f8015a2 100644 --- a/tests/Controller/OpenIdConnectInstallerControllerTest.php +++ b/tests/src/Controller/InstallerControllerTest.php @@ -1,10 +1,14 @@ templateFactoryMock = $this->createMock(TemplateFactory::class); @@ -53,9 +42,9 @@ protected function setUp(): void $this->templateMock = $this->createMock(Template::class); } - protected function createStubbedInstance(): OpenIdConnectInstallerController + protected function createStubbedInstance(): InstallerController { - return new OpenIdConnectInstallerController( + return new InstallerController( $this->templateFactoryMock, $this->sessionMessagesService, $this->databaseMigrationMock, @@ -66,11 +55,14 @@ protected function createStubbedInstance(): OpenIdConnectInstallerController public function testItIsInitializable(): void { $this->assertInstanceOf( - OpenIdConnectInstallerController::class, + InstallerController::class, $this->createStubbedInstance() ); } + /** + * @throws \Exception + */ public function testItReturnsToMainPageIfAlreadyUpdated(): void { $this->databaseMigrationMock @@ -84,6 +76,9 @@ public function testItReturnsToMainPageIfAlreadyUpdated(): void ); } + /** + * @throws \Exception + */ public function testItShowsInformationPage(): void { $this->serverRequestMock->expects($this->once())->method('getParsedBody'); @@ -100,6 +95,9 @@ public function testItShowsInformationPage(): void ); } + /** + * @throws \Exception + */ public function testItRequiresConfirmationBeforeInstallSchema(): void { $this->serverRequestMock->expects($this->once())->method('getParsedBody'); @@ -117,6 +115,9 @@ public function testItRequiresConfirmationBeforeInstallSchema(): void ); } + /** + * @throws \Exception + */ public function testItCreatesSchema(): void { $this->serverRequestMock->expects($this->once())->method('getParsedBody')->willReturn(['migrate' => true,]); @@ -131,6 +132,9 @@ public function testItCreatesSchema(): void ); } + /** + * @throws \Exception + */ public function testItImportsDataFromOauth2Module(): void { $this->serverRequestMock @@ -143,9 +147,11 @@ public function testItImportsDataFromOauth2Module(): void $this->sessionMessagesService ->expects($this->atLeast(2)) ->method('addMessage') - ->with($this->callback(function ($message) { - return in_array($message, ['{oidc:install:finished}', '{oidc:import:finished}']); - })); + ->with( + $this->callback( + fn($message) => in_array($message, ['{oidc:install:finished}', '{oidc:import:finished}']) + ) + ); $this->assertInstanceOf( RedirectResponse::class, diff --git a/tests/Controller/OpenIdConnectJwksControllerTest.php b/tests/src/Controller/JwksControllerTest.php similarity index 61% rename from tests/Controller/OpenIdConnectJwksControllerTest.php rename to tests/src/Controller/JwksControllerTest.php index fe5bb672..efffe479 100644 --- a/tests/Controller/OpenIdConnectJwksControllerTest.php +++ b/tests/src/Controller/JwksControllerTest.php @@ -1,26 +1,27 @@ jsonWebKeySetServiceMock = $this->createMock(JsonWebKeySetService::class); @@ -30,8 +31,8 @@ protected function setUp(): void public function testItIsInitializable(): void { $this->assertInstanceOf( - OpenIdConnectJwksController::class, - new OpenIdConnectJwksController($this->jsonWebKeySetServiceMock) + JwksController::class, + new JwksController($this->jsonWebKeySetServiceMock) ); } @@ -52,8 +53,7 @@ public function testItReturnsJsonKeys(): void $this->assertSame( ['keys' => $keys], - (new OpenIdConnectJwksController($this->jsonWebKeySetServiceMock)) - ->__invoke($this->serverRequestMock) + (new JwksController($this->jsonWebKeySetServiceMock))->__invoke() ->getPayload() ); } diff --git a/tests/Controller/LogoutControllerTest.php b/tests/src/Controller/LogoutControllerTest.php similarity index 81% rename from tests/Controller/LogoutControllerTest.php rename to tests/src/Controller/LogoutControllerTest.php index f7050215..d15780b8 100644 --- a/tests/Controller/LogoutControllerTest.php +++ b/tests/src/Controller/LogoutControllerTest.php @@ -1,26 +1,29 @@ authorizationServerStub = $this->createStub(AuthorizationServer::class); - $this->authenticationServiceStub = $this->createStub(AuthenticationService::class); $this->sessionServiceStub = $this->createStub(SessionService::class); - $this->sessionLogoutTicketStoreBuilderStub = $this->createStub(SessionLogoutTicketStoreBuilder::class); + $this->sessionLogoutTicketStoreBuilderStub = $this->createStub(LogoutTicketStoreBuilder::class); $this->serverRequestStub = $this->createStub(ServerRequest::class); $this->currentSessionMock = $this->createMock(Session::class); $this->sessionMock = $this->createMock(Session::class); @@ -102,7 +62,7 @@ public function setUp(): void $this->idTokenHintStub = $this->createStub(UnencryptedToken::class); $this->dataSet = new DataSet(['sid' => '123'], ''); $this->loggerServiceMock = $this->createMock(LoggerService::class); - $this->sessionLogoutTicketStoreDbStub = $this->createStub(SessionLogoutTicketStoreDb::class); + $this->sessionLogoutTicketStoreDbStub = $this->createStub(LogoutTicketStoreDb::class); $this->templateFactoryStub = $this->createStub(TemplateFactory::class); } @@ -112,7 +72,6 @@ public function testConstruct(): void LogoutController::class, new LogoutController( $this->authorizationServerStub, - $this->authenticationServiceStub, $this->sessionServiceStub, $this->sessionLogoutTicketStoreBuilderStub, $this->loggerServiceMock, @@ -132,7 +91,6 @@ public function testInvokeThrowsForInvalidLogoutRequest(): void $logoutController = new LogoutController( $this->authorizationServerStub, - $this->authenticationServiceStub, $this->sessionServiceStub, $this->sessionLogoutTicketStoreBuilderStub, $this->loggerServiceMock, @@ -164,14 +122,10 @@ public function testCallLogoutForSessionIdInIdTokenHint(): void $this->sessionMock->expects($this->exactly(2)) ->method('doLogout') - ->withConsecutive( - ['authId1'], - ['authId2'] - ); + ->with($this->callback(fn($authId) => in_array($authId, ['authId1', 'authId2']))); (new LogoutController( $this->authorizationServerStub, - $this->authenticationServiceStub, $this->sessionServiceStub, $this->sessionLogoutTicketStoreBuilderStub, $this->loggerServiceMock, @@ -202,7 +156,6 @@ public function testLogsIfSessionFromIdTokenHintNotFound(): void (new LogoutController( $this->authorizationServerStub, - $this->authenticationServiceStub, $this->sessionServiceStub, $this->sessionLogoutTicketStoreBuilderStub, $this->loggerServiceMock, @@ -221,16 +174,12 @@ public function testLogoutCalledOnCurrentSession(): void $this->currentSessionMock->expects($this->exactly(2)) ->method('doLogout') - ->withConsecutive( - ['authId1'], - ['authId2'] - ); + ->with($this->callback(fn($authId) => in_array($authId, ['authId1', 'authId2']))); $this->sessionServiceStub->method('getCurrentSession')->willReturn($this->currentSessionMock); (new LogoutController( $this->authorizationServerStub, - $this->authenticationServiceStub, $this->sessionServiceStub, $this->sessionLogoutTicketStoreBuilderStub, $this->loggerServiceMock, @@ -253,7 +202,6 @@ public function testReturnsRedirectResponseIfPostLogoutRedirectUriIsSet(): void $logoutController = new LogoutController( $this->authorizationServerStub, - $this->authenticationServiceStub, $this->sessionServiceStub, $this->sessionLogoutTicketStoreBuilderStub, $this->loggerServiceMock, @@ -275,7 +223,6 @@ public function testReturnsResponse(): void $logoutController = new LogoutController( $this->authorizationServerStub, - $this->authenticationServiceStub, $this->sessionServiceStub, $this->sessionLogoutTicketStoreBuilderStub, $this->loggerServiceMock, @@ -285,7 +232,7 @@ public function testReturnsResponse(): void $this->assertInstanceOf(Response::class, $logoutController->__invoke($this->serverRequestStub)); } - public function testLogoutHandler(): void + public function testLogoutHandler(): never { $this->markTestIncomplete(); } diff --git a/tests/Controller/OpenIdConnectUserInfoControllerTest.php b/tests/src/Controller/UserInfoControllerTest.php similarity index 84% rename from tests/Controller/OpenIdConnectUserInfoControllerTest.php rename to tests/src/Controller/UserInfoControllerTest.php index 7e3094c6..afc5e9ba 100644 --- a/tests/Controller/OpenIdConnectUserInfoControllerTest.php +++ b/tests/src/Controller/UserInfoControllerTest.php @@ -1,63 +1,44 @@ resourceServerMock = $this->createMock(ResourceServer::class); @@ -72,9 +53,9 @@ protected function setUp(): void $this->userEntityMock = $this->createMock(UserEntity::class); } - protected function prepareMockedInstance(): OpenIdConnectUserInfoController + protected function prepareMockedInstance(): UserInfoController { - return new OpenIdConnectUserInfoController( + return new UserInfoController( $this->resourceServerMock, $this->accessTokenRepositoryMock, $this->userRepositoryMock, @@ -86,11 +67,16 @@ protected function prepareMockedInstance(): OpenIdConnectUserInfoController public function testItIsInitializable(): void { $this->assertInstanceOf( - OpenIdConnectUserInfoController::class, + UserInfoController::class, $this->prepareMockedInstance() ); } + /** + * @throws UserNotFound + * @throws OidcServerException + * @throws OAuthServerException + */ public function testItReturnsUserClaims(): void { $this->serverRequestMock->expects($this->once())->method('getMethod')->willReturn('GET'); @@ -145,13 +131,16 @@ public function testItReturnsUserClaims(): void ->with([], ['mail' => ['userid@localhost.localdomain']]) ->willReturn([]); - /** @psalm-suppress UndefinedMethod */ $this->assertSame( ['email' => 'userid@localhost.localdomain'], $this->prepareMockedInstance()->__invoke($this->serverRequestMock)->getPayload() ); } + /** + * @throws OidcServerException + * @throws OAuthServerException + */ public function testItThrowsIfAccessTokenNotFound(): void { $this->serverRequestMock->expects($this->once())->method('getMethod')->willReturn('GET'); @@ -183,6 +172,10 @@ public function testItThrowsIfAccessTokenNotFound(): void $this->prepareMockedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws OidcServerException + * @throws OAuthServerException + */ public function testItThrowsIfUserNotFound(): void { $this->serverRequestMock->expects($this->once())->method('getMethod')->willReturn('GET'); @@ -223,6 +216,11 @@ public function testItThrowsIfUserNotFound(): void $this->prepareMockedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws UserNotFound + * @throws OidcServerException + * @throws OAuthServerException + */ public function testItHandlesCorsRequest(): void { $origin = 'https://example.org'; @@ -241,6 +239,10 @@ public function testItHandlesCorsRequest(): void ); } + /** + * @throws UserNotFound + * @throws OAuthServerException + */ public function testItThrowsIfCorsOriginNotAllowed(): void { $origin = 'https://example.org'; @@ -252,6 +254,10 @@ public function testItThrowsIfCorsOriginNotAllowed(): void $this->prepareMockedInstance()->__invoke($this->serverRequestMock); } + /** + * @throws UserNotFound + * @throws OAuthServerException + */ public function testItThrowsIfOriginHeaderNotAvailable(): void { $this->serverRequestMock->expects($this->once())->method('getMethod')->willReturn('OPTIONS'); diff --git a/tests/Entity/AccessTokenEntityTest.php b/tests/src/Entities/AccessTokenEntityTest.php similarity index 77% rename from tests/Entity/AccessTokenEntityTest.php rename to tests/src/Entities/AccessTokenEntityTest.php index f5e95250..342923fe 100644 --- a/tests/Entity/AccessTokenEntityTest.php +++ b/tests/src/Entities/AccessTokenEntityTest.php @@ -1,16 +1,20 @@ dirname(__DIR__, 2) . '/docker/ssp/', + 'certdir' => dirname(__DIR__, 3) . '/docker/ssp/', ]; Configuration::loadFromArray($config, '', 'simplesaml'); @@ -74,16 +83,20 @@ protected function setUp(): void $this->state = [ 'id' => $this->id, - 'scopes' => json_encode($this->scopes), + 'scopes' => json_encode($this->scopes, JSON_THROW_ON_ERROR), 'expires_at' => $this->expiresAt, 'user_id' => $this->userId, 'client' => $this->clientEntityStub, 'is_revoked' => $this->isRevoked, 'auth_code_id' => $this->authCodeId, - 'requested_claims' => json_encode($this->requestedClaims) + 'requested_claims' => json_encode($this->requestedClaims, JSON_THROW_ON_ERROR) ]; } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testCanCreateInstanceFromState(): void { $this->assertInstanceOf(AccessTokenEntity::class, AccessTokenEntity::fromState($this->state)); @@ -103,17 +116,25 @@ public function testCanCreateInstanceFromData(): void ); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testHasProperState(): void { $accessTokenEntity = AccessTokenEntity::fromState($this->state); $accessTokenEntityState = $accessTokenEntity->getState(); $this->assertSame($this->id, $accessTokenEntityState['id']); - $this->assertSame(json_encode($this->scopes), $accessTokenEntityState['scopes']); + $this->assertSame(json_encode($this->scopes, JSON_THROW_ON_ERROR), $accessTokenEntityState['scopes']); $this->assertSame($this->requestedClaims, $accessTokenEntity->getRequestedClaims()); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testHasImmutableStringRepresentation(): void { $accessTokenEntity = AccessTokenEntity::fromState($this->state); diff --git a/tests/Entity/AuthCodeEntityTest.php b/tests/src/Entities/AuthCodeEntityTest.php similarity index 70% rename from tests/Entity/AuthCodeEntityTest.php rename to tests/src/Entities/AuthCodeEntityTest.php index 5da4bcf3..f49642ca 100644 --- a/tests/Entity/AuthCodeEntityTest.php +++ b/tests/src/Entities/AuthCodeEntityTest.php @@ -1,22 +1,28 @@ clientEntityMock = $this->createMock(ClientEntity::class); @@ -33,12 +39,20 @@ protected function setUp(): void ]; } + /** + * @throws OidcServerException + * @throws JsonException + */ protected function prepareMockedInstance(array $state = null): AuthCodeEntity { - $state = $state ?? $this->state; + $state ??= $this->state; return AuthCodeEntity::fromState($state); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testItIsInitializable(): void { $this->assertInstanceOf( @@ -47,6 +61,10 @@ public function testItIsInitializable(): void ); } + /** + * @throws JsonException + * @throws OidcServerException + */ public function testCanGetState(): void { $this->assertSame( @@ -64,6 +82,10 @@ public function testCanGetState(): void ); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testCanSetNonce(): void { $authCodeEntity = $this->prepareMockedInstance(); @@ -72,6 +94,10 @@ public function testCanSetNonce(): void $this->assertSame('new_nonce', $authCodeEntity->getNonce()); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testCanBeRevoked(): void { $authCodeEntity = $this->prepareMockedInstance(); diff --git a/tests/Entity/ClientEntityTest.php b/tests/src/Entities/ClientEntityTest.php similarity index 84% rename from tests/Entity/ClientEntityTest.php rename to tests/src/Entities/ClientEntityTest.php index f30ad9bb..e2125713 100644 --- a/tests/Entity/ClientEntityTest.php +++ b/tests/src/Entities/ClientEntityTest.php @@ -1,12 +1,16 @@ state; + $state ??= $this->state; return ClientEntity::fromState($state); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testItIsInitializable(): void { $this->assertInstanceOf( @@ -48,6 +60,10 @@ public function testItIsInitializable(): void ); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testCanGetProperties(): void { $clientEntity = $this->prepareMockedInstance(); @@ -71,6 +87,10 @@ public function testCanGetProperties(): void $this->assertSame('https://localhost/back', $clientEntity->getBackChannelLogoutUri()); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testCanChangeSecret(): void { $clientEntity = $this->prepareMockedInstance(); @@ -79,6 +99,10 @@ public function testCanChangeSecret(): void $this->assertSame($clientEntity->getSecret(), 'new_secret'); } + /** + * @throws JsonException + * @throws OidcServerException + */ public function testCanGetState(): void { $this->assertSame( @@ -100,6 +124,10 @@ public function testCanGetState(): void ); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testCanExportAsArray(): void { $this->assertSame( diff --git a/tests/Entity/RefreshTokenEntityTest.php b/tests/src/Entities/RefreshTokenEntityTest.php similarity index 64% rename from tests/Entity/RefreshTokenEntityTest.php rename to tests/src/Entities/RefreshTokenEntityTest.php index 402c87f8..26265730 100644 --- a/tests/Entity/RefreshTokenEntityTest.php +++ b/tests/src/Entities/RefreshTokenEntityTest.php @@ -1,23 +1,28 @@ accessTokenEntityMock = $this->createMock(AccessTokenEntity::class); @@ -32,12 +37,18 @@ protected function setUp(): void ]; } + /** + * @throws OidcServerException + */ protected function prepareMockedInstance(array $state = null): RefreshTokenEntityInterface { - $state = $state ?? $this->state; + $state ??= $this->state; return RefreshTokenEntity::fromState($state); } + /** + * @throws OidcServerException + */ public function testItIsInitializable(): void { $this->assertInstanceOf( @@ -46,6 +57,9 @@ public function testItIsInitializable(): void ); } + /** + * @throws OidcServerException + */ public function testCanGetState(): void { $this->assertSame( diff --git a/tests/Entity/ScopeEntityTest.php b/tests/src/Entities/ScopeEntityTest.php similarity index 88% rename from tests/Entity/ScopeEntityTest.php rename to tests/src/Entities/ScopeEntityTest.php index 50a7ecb5..a3b6fa19 100644 --- a/tests/Entity/ScopeEntityTest.php +++ b/tests/src/Entities/ScopeEntityTest.php @@ -1,8 +1,10 @@ assertSame('id', $scopeEntity->getIdentifier()); $this->assertSame('description', $scopeEntity->getDescription()); $this->assertSame('icon', $scopeEntity->getIcon()); - $this->assertSame(['attrid' => 'attrval'], $scopeEntity->getAttributes()); + $this->assertSame(['attrid' => 'attrval'], $scopeEntity->getClaims()); $this->assertSame('id', $scopeEntity->jsonSerialize()); } } diff --git a/tests/Entity/UserEntityTest.php b/tests/src/Entities/UserEntityTest.php similarity index 75% rename from tests/Entity/UserEntityTest.php rename to tests/src/Entities/UserEntityTest.php index fa9e3f3b..4c7a81bb 100644 --- a/tests/Entity/UserEntityTest.php +++ b/tests/src/Entities/UserEntityTest.php @@ -1,12 +1,16 @@ state; + $state ??= $this->state; return UserEntity::fromState($state); } + /** + * @throws OidcServerException + * @throws Exception + */ public function testItIsInitializable(): void { $this->assertInstanceOf( @@ -41,6 +52,10 @@ public function testItIsInitializable(): void ); } + /** + * @throws OidcServerException + * @throws Exception + */ public function testCanGetProperties(): void { $userEntity = $this->prepareMockedInstance(); @@ -53,6 +68,9 @@ public function testCanGetProperties(): void $this->assertSame($userEntity->getClaims(), ['claim']); } + /** + * @throws OidcServerException + */ public function testCanGetState(): void { $this->assertSame( diff --git a/tests/Factories/AuthSimpleFactoryTest.php b/tests/src/Factories/AuthSimpleFactoryTest.php similarity index 81% rename from tests/Factories/AuthSimpleFactoryTest.php rename to tests/src/Factories/AuthSimpleFactoryTest.php index 0e340031..2463ee09 100644 --- a/tests/Factories/AuthSimpleFactoryTest.php +++ b/tests/src/Factories/AuthSimpleFactoryTest.php @@ -1,5 +1,7 @@ markTestIncomplete(); } diff --git a/tests/Factories/ClaimTranslatorExtractorFactoryTest.php b/tests/src/Factories/ClaimTranslatorExtractorFactoryTest.php similarity index 80% rename from tests/Factories/ClaimTranslatorExtractorFactoryTest.php rename to tests/src/Factories/ClaimTranslatorExtractorFactoryTest.php index 04d13186..b77f4060 100644 --- a/tests/Factories/ClaimTranslatorExtractorFactoryTest.php +++ b/tests/src/Factories/ClaimTranslatorExtractorFactoryTest.php @@ -1,33 +1,37 @@ configurationServiceMock = $this->createMock(ConfigurationService::class); - $this->configurationServiceMock - ->method('getOpenIDConnectConfiguration') + $this->moduleConfigMock = $this->createMock(ModuleConfig::class); + $this->moduleConfigMock + ->method('config') ->willReturn( Configuration::loadFromArray( [ - 'useridattr' => 'uid', - 'translate' => [ + ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE => 'uid', + ModuleConfig::OPTION_AUTH_SAML_TO_OIDC_TRANSLATE_TABLE => [ 'testClaim' => ['attribute'], 'intClaim' => [ 'type' => 'int', @@ -42,7 +46,7 @@ protected function setUp(): void ] ) ); - $this->configurationServiceMock + $this->moduleConfigMock ->method('getOpenIDPrivateScopes') ->willReturn( [ @@ -63,7 +67,7 @@ protected function setUp(): void protected function prepareMockedInstance(): ClaimTranslatorExtractorFactory { - return new ClaimTranslatorExtractorFactory($this->configurationServiceMock); + return new ClaimTranslatorExtractorFactory($this->moduleConfigMock); } public function testCanCreateInstance(): void @@ -74,6 +78,9 @@ public function testCanCreateInstance(): void ); } + /** + * @throws \Exception + */ public function testCanBuildClaimTranslatorExtractor(): void { $this->assertInstanceOf( @@ -82,11 +89,13 @@ public function testCanBuildClaimTranslatorExtractor(): void ); } + /** + * @throws \Exception + */ public function testExtractor(): void { $claimTranslatorExtractor = $this->prepareMockedInstance()->build(); - /** @psalm-suppress PossiblyNullReference */ $this->assertSame( $claimTranslatorExtractor->getClaimSet('customScope2')->getClaims(), ['myprefix_testClaim2', 'myprefix_boolClaim'] diff --git a/tests/src/Forms/ClientFormTest.php b/tests/src/Forms/ClientFormTest.php new file mode 100644 index 00000000..244ca520 --- /dev/null +++ b/tests/src/Forms/ClientFormTest.php @@ -0,0 +1,19 @@ +markTestIncomplete(); + } +} diff --git a/tests/src/ModuleConfigTest.php b/tests/src/ModuleConfigTest.php new file mode 100644 index 00000000..f1a45c08 --- /dev/null +++ b/tests/src/ModuleConfigTest.php @@ -0,0 +1,57 @@ + $certDir + ] + ) + ); + Configuration::setPreLoadedConfig( + Configuration::loadFromArray([]), + ModuleConfig::DEFAULT_FILE_NAME + ); + // Test default cert and pem + $moduleConfig = new ModuleConfig(); + $this->assertEquals($certDir . ModuleConfig::DEFAULT_PKI_CERTIFICATE_FILENAME, $moduleConfig->getCertPath()); + $this->assertEquals( + $certDir . ModuleConfig::DEFAULT_PKI_PRIVATE_KEY_FILENAME, + $moduleConfig->getPrivateKeyPath() + ); + + // Set customized + Configuration::setPreLoadedConfig( + Configuration::loadFromArray( + [ + ModuleConfig::OPTION_PKI_PRIVATE_KEY_FILENAME => 'myPrivateKey.key', + ModuleConfig::OPTION_PKI_CERTIFICATE_FILENAME => 'myCertificate.crt', + ] + ), + ModuleConfig::DEFAULT_FILE_NAME + ); + $moduleConfig = new ModuleConfig(); + $this->assertEquals($certDir . 'myCertificate.crt', $moduleConfig->getCertPath()); + $this->assertEquals($certDir . 'myPrivateKey.key', $moduleConfig->getPrivateKeyPath()); + } +} diff --git a/tests/Repositories/AccessTokenRepositoryTest.php b/tests/src/Repositories/AccessTokenRepositoryTest.php similarity index 67% rename from tests/Repositories/AccessTokenRepositoryTest.php rename to tests/src/Repositories/AccessTokenRepositoryTest.php index de6b213a..4a7327cf 100644 --- a/tests/Repositories/AccessTokenRepositoryTest.php +++ b/tests/src/Repositories/AccessTokenRepositoryTest.php @@ -1,5 +1,7 @@ migrate(); - $configurationService = new ConfigurationService(); + $moduleConfig = new ModuleConfig(); $client = ClientRepositoryTest::getClient(self::CLIENT_ID); - (new ClientRepository($configurationService))->add($client); + (new ClientRepository($moduleConfig))->add($client); $user = UserEntity::fromData(self::USER_ID); - (new UserRepository($configurationService))->add($user); + (new UserRepository($moduleConfig))->add($user); - self::$repository = new AccessTokenRepository($configurationService); + self::$repository = new AccessTokenRepository($moduleConfig); } public function testGetTableName(): void @@ -68,6 +75,13 @@ public function testGetTableName(): void $this->assertSame('phpunit_oidc_access_token', self::$repository->getTableName()); } + /** + * @throws UniqueTokenIdentifierConstraintViolationException + * @throws Error + * @throws OidcServerException + * @throws JsonException + * @throws Exception + */ public function testAddAndFound(): void { $scopes = [ @@ -80,7 +94,7 @@ public function testAddAndFound(): void self::USER_ID ); $accessToken->setIdentifier(self::ACCESS_TOKEN_ID); - $accessToken->setExpiryDateTime(\DateTimeImmutable::createFromMutable( + $accessToken->setExpiryDateTime(DateTimeImmutable::createFromMutable( TimestampGenerator::utc('yesterday') )); @@ -91,6 +105,9 @@ public function testAddAndFound(): void $this->assertEquals($accessToken, $foundAccessToken); } + /** + * @throws OidcServerException + */ public function testAddAndNotFound(): void { $notFoundAccessToken = self::$repository->findById('notoken'); @@ -98,6 +115,10 @@ public function testAddAndNotFound(): void $this->assertNull($notFoundAccessToken); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testRevokeToken(): void { self::$repository->revokeAccessToken(self::ACCESS_TOKEN_ID); @@ -106,20 +127,31 @@ public function testRevokeToken(): void $this->assertTrue($isRevoked); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testErrorRevokeInvalidToken(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(Exception::class); self::$repository->revokeAccessToken('notoken'); } + /** + * @throws OidcServerException + */ public function testErrorCheckIsRevokedInvalidToken(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(Exception::class); self::$repository->isAccessTokenRevoked('notoken'); } + /** + * @throws OidcServerException + * @throws Exception + */ public function testRemoveExpired(): void { self::$repository->removeExpired(); diff --git a/tests/Repositories/AllowedOriginRepositoryTest.php b/tests/src/Repositories/AllowedOriginRepositoryTest.php similarity index 79% rename from tests/Repositories/AllowedOriginRepositoryTest.php rename to tests/src/Repositories/AllowedOriginRepositoryTest.php index 00a17019..2fedf975 100644 --- a/tests/Repositories/AllowedOriginRepositoryTest.php +++ b/tests/src/Repositories/AllowedOriginRepositoryTest.php @@ -1,12 +1,15 @@ migrate(); - self::$configurationService = new ConfigurationService(); + self::$moduleConfig = new ModuleConfig(); $client = ClientRepositoryTest::getClient(self::CLIENT_ID); - (new ClientRepository(self::$configurationService))->add($client); + (new ClientRepository(self::$moduleConfig))->add($client); - self::$repository = new AllowedOriginRepository(self::$configurationService); + self::$repository = new AllowedOriginRepository(self::$moduleConfig); } public function tearDown(): void diff --git a/tests/Repositories/AuthCodeRepositoryTest.php b/tests/src/Repositories/AuthCodeRepositoryTest.php similarity index 69% rename from tests/Repositories/AuthCodeRepositoryTest.php rename to tests/src/Repositories/AuthCodeRepositoryTest.php index b190a55d..5efa3e51 100644 --- a/tests/Repositories/AuthCodeRepositoryTest.php +++ b/tests/src/Repositories/AuthCodeRepositoryTest.php @@ -1,5 +1,7 @@ migrate(); - $configurationService = new ConfigurationService(); + $moduleConfig = new ModuleConfig(); $client = ClientRepositoryTest::getClient(self::CLIENT_ID); - (new ClientRepository($configurationService))->add($client); + (new ClientRepository($moduleConfig))->add($client); $user = UserEntity::fromData(self::USER_ID); - (new UserRepository($configurationService))->add($user); + (new UserRepository($moduleConfig))->add($user); - self::$repository = new AuthCodeRepository($configurationService); + self::$repository = new AuthCodeRepository($moduleConfig); } public function testGetTableName(): void @@ -69,6 +75,13 @@ public function testGetTableName(): void $this->assertSame('phpunit_oidc_auth_code', self::$repository->getTableName()); } + /** + * @throws UniqueTokenIdentifierConstraintViolationException + * @throws Error + * @throws JsonException + * @throws Exception + * @throws Exception + */ public function testAddAndFound(): void { $scopes = [ @@ -80,7 +93,7 @@ public function testAddAndFound(): void $authCode->setIdentifier(self::AUTH_CODE_ID); $authCode->setClient(ClientRepositoryTest::getClient(self::CLIENT_ID)); $authCode->setUserIdentifier(self::USER_ID); - $authCode->setExpiryDateTime(\DateTimeImmutable::createFromMutable(TimestampGenerator::utc('yesterday'))); + $authCode->setExpiryDateTime(DateTimeImmutable::createFromMutable(TimestampGenerator::utc('yesterday'))); $authCode->setRedirectUri(self::REDIRECT_URI); foreach ($scopes as $scope) { $authCode->addScope($scope); @@ -93,6 +106,9 @@ public function testAddAndFound(): void $this->assertEquals($authCode, $foundAuthCode); } + /** + * @throws Exception + */ public function testAddAndNotFound(): void { $notFoundAuthCode = self::$repository->findById('nocode'); @@ -100,6 +116,10 @@ public function testAddAndNotFound(): void $this->assertNull($notFoundAuthCode); } + /** + * @throws JsonException + * @throws Exception + */ public function testRevokeCode(): void { self::$repository->revokeAuthCode(self::AUTH_CODE_ID); @@ -108,20 +128,26 @@ public function testRevokeCode(): void $this->assertTrue($isRevoked); } + /** + * @throws JsonException + */ public function testErrorRevokeInvalidAuthCode(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(Exception::class); self::$repository->revokeAuthCode('nocode'); } public function testErrorCheckIsRevokedInvalidAuthCode(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(Exception::class); self::$repository->isAuthCodeRevoked('nocode'); } + /** + * @throws Exception + */ public function testRemoveExpired(): void { self::$repository->removeExpired(); diff --git a/tests/Repositories/ClientRepositoryTest.php b/tests/src/Repositories/ClientRepositoryTest.php similarity index 83% rename from tests/Repositories/ClientRepositoryTest.php rename to tests/src/Repositories/ClientRepositoryTest.php index 95d15541..b8b7ec3c 100644 --- a/tests/Repositories/ClientRepositoryTest.php +++ b/tests/src/Repositories/ClientRepositoryTest.php @@ -1,5 +1,7 @@ migrate(); - $configurationService = new ConfigurationService(); + $moduleConfig = new ModuleConfig(); - self::$repository = new ClientRepository($configurationService); + self::$repository = new ClientRepository($moduleConfig); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function tearDown(): void { $clients = self::$repository->findAll(); @@ -66,6 +74,10 @@ public function testGetTableName(): void $this->assertSame('phpunit_oidc_client', self::$repository->getTableName()); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testAddAndFound(): void { $client = self::getClient('clientid'); @@ -75,6 +87,10 @@ public function testAddAndFound(): void $this->assertEquals($client, $foundClient); } + /** + * @throws OAuthServerException + * @throws JsonException + */ public function testGetClientEntity(): void { $client = self::getClient('clientid'); @@ -84,6 +100,9 @@ public function testGetClientEntity(): void $this->assertNotNull($client); } + /** + * @throws JsonException + */ public function testGetDisabledClientEntity(): void { $this->expectException(OAuthServerException::class); @@ -91,9 +110,13 @@ public function testGetDisabledClientEntity(): void $client = self::getClient('clientid', false); self::$repository->add($client); - $client = self::$repository->getClientEntity('clientid'); + self::$repository->getClientEntity('clientid'); } + /** + * @throws OAuthServerException + * @throws JsonException + */ public function testNotFoundClient(): void { $client = self::$repository->getClientEntity('unknownid'); @@ -101,6 +124,10 @@ public function testNotFoundClient(): void $this->assertNull($client); } + /** + * @throws OAuthServerException + * @throws JsonException + */ public function testValidateConfidentialClient(): void { $client = self::getClient('clientid', true, true); @@ -110,15 +137,23 @@ public function testValidateConfidentialClient(): void $this->assertTrue($validate); } + /** + * @throws OAuthServerException + * @throws JsonException + */ public function testValidatePublicClient(): void { - $client = self::getClient('clientid', true); + $client = self::getClient('clientid'); self::$repository->add($client); $validate = self::$repository->validateClient('clientid', null, null); $this->assertTrue($validate); } + /** + * @throws OAuthServerException + * @throws JsonException + */ public function testNotValidateConfidentialClientWithWrongSecret() { $client = self::getClient('clientid', true, true); @@ -128,12 +163,20 @@ public function testNotValidateConfidentialClientWithWrongSecret() $this->assertFalse($validate); } + /** + * @throws OAuthServerException + * @throws JsonException + */ public function testNotValidateWhenClientDoesNotExists() { $validate = self::$repository->validateClient('clientid', 'wrongclientsecret', null); $this->assertFalse($validate); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testFindAll(): void { $client = self::getClient('clientid'); @@ -144,13 +187,16 @@ public function testFindAll(): void $this->assertInstanceOf(ClientEntity::class, current($clients)); } + /** + * @throws Exception + */ public function testFindPaginated(): void { array_map(function ($i) { self::$repository->add(self::getClient('clientid' . $i)); }, range(1, 21)); - $clientPageOne = self::$repository->findPaginated(1); + $clientPageOne = self::$repository->findPaginated(); self::assertCount(20, $clientPageOne['items']); self::assertEquals(2, $clientPageOne['numPages']); self::assertEquals(1, $clientPageOne['currentPage']); @@ -160,6 +206,9 @@ public function testFindPaginated(): void self::assertEquals(2, $clientPageTwo['currentPage']); } + /** + * @throws Exception + */ public function testFindPageInRange(): void { array_map(function ($i) { @@ -172,6 +221,9 @@ public function testFindPageInRange(): void self::assertEquals(2, $clientPageOne['currentPage']); } + /** + * @throws Exception + */ public function testFindPaginationWithEmptyList() { $clientPageOne = self::$repository->findPaginated(0); @@ -180,6 +232,10 @@ public function testFindPaginationWithEmptyList() self::assertCount(0, $clientPageOne['items']); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testUpdate(): void { $client = self::getClient('clientid'); @@ -203,19 +259,28 @@ public function testUpdate(): void $this->assertEquals($client, $foundClient); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testDelete(): void { $client = self::getClient('clientid'); self::$repository->add($client); $client = self::$repository->findById('clientid'); - /** @psalm-suppress PossiblyNullArgument */ self::$repository->delete($client); $foundClient = self::$repository->findById('clientid'); $this->assertNull($foundClient); } + /** + * @throws JsonException + * @throws OidcServerException + * @throws Exception + * @throws Exception + */ public function testCrudWithOwner(): void { $owner = 'homer@example.com'; diff --git a/tests/Repositories/RefreshTokenRepositoryTest.php b/tests/src/Repositories/RefreshTokenRepositoryTest.php similarity index 60% rename from tests/Repositories/RefreshTokenRepositoryTest.php rename to tests/src/Repositories/RefreshTokenRepositoryTest.php index c884e21e..08ae8eca 100644 --- a/tests/Repositories/RefreshTokenRepositoryTest.php +++ b/tests/src/Repositories/RefreshTokenRepositoryTest.php @@ -1,5 +1,7 @@ migrate(); - $configurationService = new ConfigurationService(); + $moduleConfig = new ModuleConfig(); $client = ClientRepositoryTest::getClient(self::CLIENT_ID); - (new ClientRepository($configurationService))->add($client); + (new ClientRepository($moduleConfig))->add($client); $user = UserEntity::fromData(self::USER_ID); - (new UserRepository($configurationService))->add($user); + (new UserRepository($moduleConfig))->add($user); $accessToken = AccessTokenEntity::fromData($client, [], self::USER_ID); $accessToken->setIdentifier(self::ACCESS_TOKEN_ID); - $accessToken->setExpiryDateTime(new \DateTimeImmutable('yesterday')); - (new AccessTokenRepository($configurationService))->persistNewAccessToken($accessToken); + $accessToken->setExpiryDateTime(new DateTimeImmutable('yesterday')); + (new AccessTokenRepository($moduleConfig))->persistNewAccessToken($accessToken); - self::$repository = new RefreshTokenRepository($configurationService); + self::$repository = new RefreshTokenRepository($moduleConfig); } public function testGetTableName(): void @@ -75,15 +87,21 @@ public function testGetTableName(): void $this->assertSame('phpunit_oidc_refresh_token', self::$repository->getTableName()); } + /** + * @throws UniqueTokenIdentifierConstraintViolationException + * @throws OidcServerException + * @throws OAuthServerException + * @throws Exception + * @throws Exception + */ public function testAddAndFound(): void { - $configurationService = new ConfigurationService(); - $accessToken = (new AccessTokenRepository($configurationService))->findById(self::ACCESS_TOKEN_ID); + $moduleConfig = new ModuleConfig(); + $accessToken = (new AccessTokenRepository($moduleConfig))->findById(self::ACCESS_TOKEN_ID); $refreshToken = self::$repository->getNewRefreshToken(); $refreshToken->setIdentifier(self::REFRESH_TOKEN_ID); - $refreshToken->setExpiryDateTime(\DateTimeImmutable::createFromMutable(TimestampGenerator::utc('yesterday'))); - /** @psalm-suppress PossiblyNullArgument */ + $refreshToken->setExpiryDateTime(DateTimeImmutable::createFromMutable(TimestampGenerator::utc('yesterday'))); $refreshToken->setAccessToken($accessToken); self::$repository->persistNewRefreshToken($refreshToken); @@ -93,6 +111,9 @@ public function testAddAndFound(): void $this->assertEquals($refreshToken, $foundRefreshToken); } + /** + * @throws OidcServerException + */ public function testAddAndNotFound(): void { $notFoundRefreshToken = self::$repository->findById('notoken'); @@ -100,6 +121,9 @@ public function testAddAndNotFound(): void $this->assertNull($notFoundRefreshToken); } + /** + * @throws OidcServerException + */ public function testRevokeToken(): void { self::$repository->revokeRefreshToken(self::REFRESH_TOKEN_ID); @@ -108,20 +132,30 @@ public function testRevokeToken(): void $this->assertTrue($isRevoked); } + /** + * @throws OidcServerException + */ public function testErrorRevokeInvalidToken(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); self::$repository->revokeRefreshToken('notoken'); } + /** + * @throws OidcServerException + */ public function testErrorCheckIsRevokedInvalidToken(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); self::$repository->isRefreshTokenRevoked('notoken'); } + /** + * @throws OidcServerException + * @throws Exception + */ public function testRemoveExpired(): void { self::$repository->removeExpired(); diff --git a/tests/Repositories/ScopeRepositoryTest.php b/tests/src/Repositories/ScopeRepositoryTest.php similarity index 80% rename from tests/Repositories/ScopeRepositoryTest.php rename to tests/src/Repositories/ScopeRepositoryTest.php index d3728269..822e7348 100644 --- a/tests/Repositories/ScopeRepositoryTest.php +++ b/tests/src/Repositories/ScopeRepositoryTest.php @@ -1,5 +1,7 @@ migrate(); } + /** + * @throws Exception + */ public function testGetScopeEntityByIdentifier(): void { - $scopeRepository = new ScopeRepository(new ConfigurationService()); + $scopeRepository = new ScopeRepository(new ModuleConfig()); $scope = $scopeRepository->getScopeEntityByIdentifier('openid'); @@ -57,16 +61,22 @@ public function testGetScopeEntityByIdentifier(): void $this->assertEquals($expected, $scope); } + /** + * @throws Exception + */ public function testGetUnknownScope(): void { - $scopeRepository = new ScopeRepository(new ConfigurationService()); + $scopeRepository = new ScopeRepository(new ModuleConfig()); $this->assertNull($scopeRepository->getScopeEntityByIdentifier('none')); } + /** + * @throws Exception + */ public function testFinalizeScopes(): void { - $scopeRepository = new ScopeRepository(new ConfigurationService()); + $scopeRepository = new ScopeRepository(new ModuleConfig()); $scopes = [ ScopeEntity::fromData('openid'), ScopeEntity::fromData('basic'), diff --git a/tests/Repositories/UserRepositoryTest.php b/tests/src/Repositories/UserRepositoryTest.php similarity index 79% rename from tests/Repositories/UserRepositoryTest.php rename to tests/src/Repositories/UserRepositoryTest.php index d8c0cd9f..53c590ea 100644 --- a/tests/Repositories/UserRepositoryTest.php +++ b/tests/src/Repositories/UserRepositoryTest.php @@ -1,5 +1,7 @@ migrate(); - $configurationService = new ConfigurationService(); + $moduleConfig = new ModuleConfig(); - self::$repository = new UserRepository($configurationService); + self::$repository = new UserRepository($moduleConfig); } public function testGetTableName(): void @@ -55,16 +58,22 @@ public function testGetTableName(): void $this->assertSame('phpunit_oidc_user', self::$repository->getTableName()); } + /** + * @throws OidcServerException + * @throws Exception + */ public function testAddAndFound(): void { self::$repository->add(UserEntity::fromData('uniqueid')); $user = self::$repository->getUserEntityByIdentifier('uniqueid'); $this->assertNotNull($user); - /** @psalm-suppress PossiblyNullReference */ $this->assertSame($user->getIdentifier(), 'uniqueid'); } + /** + * @throws OidcServerException + */ public function testNotFound(): void { $user = self::$repository->getUserEntityByIdentifier('unknownid'); @@ -72,10 +81,13 @@ public function testNotFound(): void $this->assertNull($user); } + /** + * @throws OidcServerException + * @throws Exception + */ public function testUpdate(): void { $user = self::$repository->getUserEntityByIdentifier('uniqueid'); - /** @psalm-suppress PossiblyNullReference */ $user->setClaims(['uid' => ['johndoe']]); self::$repository->update($user); @@ -83,10 +95,12 @@ public function testUpdate(): void $this->assertNotSame($user, $user2); } + /** + * @throws OidcServerException + */ public function testDelete(): void { $user = self::$repository->getUserEntityByIdentifier('uniqueid'); - /** @psalm-suppress PossiblyNullArgument */ self::$repository->delete($user); $user = self::$repository->getUserEntityByIdentifier('uniqueid'); diff --git a/tests/Server/Associations/RelyingPartyAssociationTest.php b/tests/src/Server/Associations/RelyingPartyAssociationTest.php similarity index 98% rename from tests/Server/Associations/RelyingPartyAssociationTest.php rename to tests/src/Server/Associations/RelyingPartyAssociationTest.php index 041ed6f4..57d673d8 100644 --- a/tests/Server/Associations/RelyingPartyAssociationTest.php +++ b/tests/src/Server/Associations/RelyingPartyAssociationTest.php @@ -1,5 +1,7 @@ markTestIncomplete(); } diff --git a/tests/Server/Grants/AuthCodeGrantTest.php b/tests/src/Server/Grants/AuthCodeGrantTest.php similarity index 65% rename from tests/Server/Grants/AuthCodeGrantTest.php rename to tests/src/Server/Grants/AuthCodeGrantTest.php index a309c619..2b185b54 100644 --- a/tests/Server/Grants/AuthCodeGrantTest.php +++ b/tests/src/Server/Grants/AuthCodeGrantTest.php @@ -1,13 +1,18 @@ authCodeRepositoryStub = $this->createStub(AuthCodeRepositoryInterface::class); $this->accessTokenRepositoryStub = $this->createStub(AccessTokenRepositoryInterface::class); $this->refreshTokenRepositoryStub = $this->createStub(RefreshTokenRepositoryInterface::class); - $this->authCodeTtl = new \DateInterval('PT1M'); + $this->authCodeTtl = new DateInterval('PT1M'); $this->requestRulesManagerStub = $this->createStub(RequestRulesManager::class); - $this->configurationServiceStub = $this->createStub(ConfigurationService::class); + $this->moduleConfigStub = $this->createStub(ModuleConfig::class); } + /** + * @throws \Exception + */ public function testCanCreateInstance(): void { $this->assertInstanceOf( @@ -57,7 +53,6 @@ public function testCanCreateInstance(): void $this->refreshTokenRepositoryStub, $this->authCodeTtl, $this->requestRulesManagerStub, - $this->configurationServiceStub ) ); } diff --git a/tests/Server/Grants/ImplicitGrantTest.php b/tests/src/Server/Grants/ImplicitGrantTest.php similarity index 81% rename from tests/Server/Grants/ImplicitGrantTest.php rename to tests/src/Server/Grants/ImplicitGrantTest.php index 224fa39e..ccd35463 100644 --- a/tests/Server/Grants/ImplicitGrantTest.php +++ b/tests/src/Server/Grants/ImplicitGrantTest.php @@ -1,5 +1,7 @@ markTestIncomplete(); } diff --git a/tests/Server/Grants/OAuth2ImplicitGrantTest.php b/tests/src/Server/Grants/OAuth2ImplicitGrantTest.php similarity index 82% rename from tests/Server/Grants/OAuth2ImplicitGrantTest.php rename to tests/src/Server/Grants/OAuth2ImplicitGrantTest.php index 8b1132ab..4c6df02e 100644 --- a/tests/Server/Grants/OAuth2ImplicitGrantTest.php +++ b/tests/src/Server/Grants/OAuth2ImplicitGrantTest.php @@ -1,5 +1,7 @@ markTestIncomplete(); } diff --git a/tests/Server/LogoutHandlers/BackChannelLogoutHandlerTest.php b/tests/src/Server/LogoutHandlers/BackChannelLogoutHandlerTest.php similarity index 83% rename from tests/Server/LogoutHandlers/BackChannelLogoutHandlerTest.php rename to tests/src/Server/LogoutHandlers/BackChannelLogoutHandlerTest.php index d544329a..deb9ad52 100644 --- a/tests/Server/LogoutHandlers/BackChannelLogoutHandlerTest.php +++ b/tests/src/Server/LogoutHandlers/BackChannelLogoutHandlerTest.php @@ -1,7 +1,12 @@ logoutTokenBuilderMock = $this->createMock(LogoutTokenBuilder::class); @@ -35,6 +43,9 @@ public function setUp(): void $this->sampleRelyingPartyAssociation[] = $this->getSampleRelyingPartyAssociation(); } + /** + * @throws OAuthServerException + */ public function testLogsErrorForInvalidUri(): void { $this->loggerServiceMock @@ -47,6 +58,9 @@ public function testLogsErrorForInvalidUri(): void $handler->handle($this->sampleRelyingPartyAssociation); } + /** + * @throws OAuthServerException + */ public function testLogsNoticeForSuccessfulResponse(): void { $mockHandler = new MockHandler([ diff --git a/tests/Server/RequestTypes/AuthorizationRequestTest.php b/tests/src/Server/RequestTypes/AuthorizationRequestTest.php similarity index 83% rename from tests/Server/RequestTypes/AuthorizationRequestTest.php rename to tests/src/Server/RequestTypes/AuthorizationRequestTest.php index 8be452d7..2ec0e99b 100644 --- a/tests/Server/RequestTypes/AuthorizationRequestTest.php +++ b/tests/src/Server/RequestTypes/AuthorizationRequestTest.php @@ -1,5 +1,7 @@ markTestIncomplete(); } diff --git a/tests/Server/RequestTypes/LogoutRequestTest.php b/tests/src/Server/RequestTypes/LogoutRequestTest.php similarity index 92% rename from tests/Server/RequestTypes/LogoutRequestTest.php rename to tests/src/Server/RequestTypes/LogoutRequestTest.php index bc4cb883..319383f6 100644 --- a/tests/Server/RequestTypes/LogoutRequestTest.php +++ b/tests/src/Server/RequestTypes/LogoutRequestTest.php @@ -1,8 +1,12 @@ idTokenHintStub = $this->createStub(UnencryptedToken::class); diff --git a/tests/Server/ResponseTypes/IdTokenResponseTest.php b/tests/src/Server/ResponseTypes/IdTokenResponseTest.php similarity index 80% rename from tests/Server/ResponseTypes/IdTokenResponseTest.php rename to tests/src/Server/ResponseTypes/IdTokenResponseTest.php index 9c9b3ef9..09fc0a3b 100644 --- a/tests/Server/ResponseTypes/IdTokenResponseTest.php +++ b/tests/src/Server/ResponseTypes/IdTokenResponseTest.php @@ -1,7 +1,10 @@ certFolder = dirname(__DIR__, 3) . '/docker/ssp/'; + $this->certFolder = dirname(__DIR__, 4) . '/docker/ssp/'; $this->userEntity = UserEntity::fromData(self::SUBJECT, [ 'cn' => ['Homer Simpson'], 'mail' => ['myEmail@example.com'] @@ -66,7 +75,7 @@ protected function setUp(): void ScopeEntity::fromData('openid'), ScopeEntity::fromData('email'), ]; - $this->expiration = (new \DateTimeImmutable())->setTimestamp(time() + 3600); + $this->expiration = (new DateTimeImmutable())->setTimestamp(time() + 3600); $this->clientEntityMock = $this->createMock(ClientEntity::class); $this->clientEntityMock->method('getIdentifier')->willReturn(self::CLIENT_ID); @@ -84,24 +93,24 @@ protected function setUp(): void ->with(self::SUBJECT) ->willReturn($this->userEntity); - $this->configurationServiceMock = $this->createMock(ConfigurationService::class); - $this->configurationServiceMock->method('getSigner')->willReturn(new Sha256()); - $this->configurationServiceMock->method('getSimpleSAMLSelfURLHost')->willReturn(self::ISSUER); - $this->configurationServiceMock->method('getCertPath') + $this->moduleConfigMock = $this->createMock(ModuleConfig::class); + $this->moduleConfigMock->method('getSigner')->willReturn(new Sha256()); + $this->moduleConfigMock->method('getSimpleSAMLSelfURLHost')->willReturn(self::ISSUER); + $this->moduleConfigMock->method('getCertPath') ->willReturn($this->certFolder . '/oidc_module.crt'); - $this->configurationServiceMock->method('getPrivateKeyPath') + $this->moduleConfigMock->method('getPrivateKeyPath') ->willReturn($this->certFolder . '/oidc_module.key'); - $this->configurationServiceMock + $this->moduleConfigMock ->expects($this->atLeast(1)) ->method('getPrivateKeyPassPhrase'); $this->sspConfigurationMock = $this->createMock(Configuration::class); - $this->configurationServiceMock->method('getOpenIDConnectConfiguration') + $this->moduleConfigMock->method('config') ->willReturn($this->sspConfigurationMock); $this->privateKey = new CryptKey($this->certFolder . '/oidc_module.key', null, false); $this->idTokenBuilder = new IdTokenBuilder( - new JsonWebTokenBuilderService($this->configurationServiceMock), + new JsonWebTokenBuilderService($this->moduleConfigMock), new ClaimTranslatorExtractor(self::USER_ID_ATTR) ); } @@ -110,11 +119,10 @@ protected function prepareMockedInstance(): IdTokenResponse { $idTokenResponse = new IdTokenResponse( $this->identityProviderMock, - $this->configurationServiceMock, - $this->idTokenBuilder + $this->idTokenBuilder, + $this->privateKey, ); - $idTokenResponse->setPrivateKey($this->privateKey); $idTokenResponse->setNonce(null); $idTokenResponse->setAuthTime(null); $idTokenResponse->setAcr(null); @@ -131,6 +139,9 @@ public function testItIsInitializable(): void ); } + /** + * @throws Exception + */ public function testItCanGenerateResponse(): void { $this->accessTokenEntityMock->method('getRequestedClaims')->willReturn([]); @@ -144,6 +155,9 @@ public function testItCanGenerateResponse(): void $this->assertTrue($this->shouldHaveValidIdToken($body)); } + /** + * @throws Exception + */ public function testItCanGenerateResponseWithIndividualRequestedClaims(): void { $idTokenResponse = $this->prepareMockedInstance(); @@ -214,7 +228,6 @@ protected function shouldHaveValidIdToken(string $body, $expectedClaims = []): b /** @var Plain $token */ $token = (new Parser(new JoseEncoder()))->parse($result['id_token']); - /** @psalm-suppress ArgumentTypeCoercion */ $validator->assert( $token, new IdentifiedBy(self::TOKEN_ID), @@ -234,12 +247,8 @@ protected function shouldHaveValidIdToken(string $body, $expectedClaims = []): b ); } $expectedClaimsKeys = array_keys($expectedClaims); - $expectedClaimsKeys = array_merge( - ['iss', 'iat', 'jti', 'aud', 'nbf', 'exp', 'sub', 'at_hash'], - $expectedClaimsKeys - ); + $expectedClaimsKeys = ['iss', 'iat', 'jti', 'aud', 'nbf', 'exp', 'sub', 'at_hash', ...$expectedClaimsKeys]; $claims = array_keys($token->claims()->all()); - /** @psalm-suppress DocblockTypeContradiction */ if ($claims !== $expectedClaimsKeys) { throw new Exception( 'missing expected claim. Got ' . var_export($claims, true) @@ -259,7 +268,7 @@ protected function shouldHaveValidIdToken(string $body, $expectedClaims = []): b $dateWithNoMicroseconds = ['nbf', 'exp', 'iat']; foreach ($dateWithNoMicroseconds as $key) { /** - * @var DateTimeImmutable + * @var DateTimeImmutable $val */ $val = $token->claims()->get($key); //Get format representing microseconds diff --git a/tests/Server/Validators/BearerTokenValidatorTest.php b/tests/src/Server/Validators/BearerTokenValidatorTest.php similarity index 80% rename from tests/Server/Validators/BearerTokenValidatorTest.php rename to tests/src/Server/Validators/BearerTokenValidatorTest.php index 66a28d07..63a3ac0d 100644 --- a/tests/Server/Validators/BearerTokenValidatorTest.php +++ b/tests/src/Server/Validators/BearerTokenValidatorTest.php @@ -1,21 +1,27 @@ accessTokenRepositoryStub = $this->createStub(AccessTokenRepository::class); $this->serverRequest = new ServerRequest(); - $this->bearerTokenValidator = new BearerTokenValidator($this->accessTokenRepositoryStub); - $this->bearerTokenValidator->setPublicKey(self::$publicCryptKey); + $this->bearerTokenValidator = new BearerTokenValidator($this->accessTokenRepositoryStub, self::$publicCryptKey); } + /** + * @throws OidcServerException + * @throws JsonException + */ public static function setUpBeforeClass(): void { // To make lib/SimpleSAML/Utils/HTTP::getSelfURL() work... @@ -126,8 +88,8 @@ public static function setUpBeforeClass(): void file_put_contents(self::$publicKeyPath, self::$publicKey); file_put_contents(self::$privateKeyPath, self::$privateKey); - \chmod(self::$publicKeyPath, 0600); - \chmod(self::$privateKeyPath, 0600); + chmod(self::$publicKeyPath, 0600); + chmod(self::$privateKeyPath, 0600); self::$publicCryptKey = new CryptKey(self::$publicKeyPath); self::$privateCryptKey = new CryptKey(self::$privateKeyPath); @@ -174,6 +136,9 @@ public function testValidatorThrowsForNonExistentAccessToken() $this->bearerTokenValidator->validateAuthorization($this->serverRequest); } + /** + * @throws OidcServerException + */ public function testValidatesForAuthorizationHeader() { $serverRequest = $this->serverRequest->withAddedHeader('Authorization', 'Bearer ' . self::$accessToken); @@ -186,6 +151,9 @@ public function testValidatesForAuthorizationHeader() ); } + /** + * @throws OidcServerException + */ public function testValidatesForPostBodyParam() { $bodyArray = ['access_token' => self::$accessToken]; @@ -214,6 +182,10 @@ public function testThrowsForUnparsableAccessToken() $this->bearerTokenValidator->validateAuthorization($serverRequest); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testThrowsForExpiredAccessToken() { $accessTokenState = self::$accessTokenState; @@ -231,13 +203,17 @@ public function testThrowsForExpiredAccessToken() $this->bearerTokenValidator->validateAuthorization($serverRequest); } + /** + * @throws OidcServerException|\Exception + */ public function testThrowsForRevokedAccessToken() { - /** @psalm-suppress UndefinedInterfaceMethod */ $this->accessTokenRepositoryStub->method('isAccessTokenRevoked')->willReturn(true); - $bearerTokenValidator = new BearerTokenValidator($this->accessTokenRepositoryStub); - $bearerTokenValidator->setPublicKey(self::$publicCryptKey); + $bearerTokenValidator = new BearerTokenValidator( + $this->accessTokenRepositoryStub, + self::$publicCryptKey, + ); $serverRequest = $this->serverRequest->withAddedHeader('Authorization', 'Bearer ' . self::$accessToken); @@ -246,6 +222,10 @@ public function testThrowsForRevokedAccessToken() $bearerTokenValidator->validateAuthorization($serverRequest); } + /** + * @throws OidcServerException + * @throws JsonException + */ public function testThrowsForEmptyAccessTokenJti() { $accessTokenState = self::$accessTokenState; diff --git a/tests/Services/AuthContextServiceTest.php b/tests/src/Services/AuthContextServiceTest.php similarity index 72% rename from tests/Services/AuthContextServiceTest.php rename to tests/src/Services/AuthContextServiceTest.php index 48f3b37c..6d181ed3 100644 --- a/tests/Services/AuthContextServiceTest.php +++ b/tests/src/Services/AuthContextServiceTest.php @@ -1,30 +1,37 @@ ['myUsername'], 'someEntitlement' => ['val1', 'val2', 'val3'] ]; protected Configuration $permissions; - protected \PHPUnit\Framework\MockObject\MockObject $oidcConfigurationMock; - protected \PHPUnit\Framework\MockObject\MockObject $configurationServiceMock; - protected \PHPUnit\Framework\MockObject\MockObject $authSimpleService; - protected \PHPUnit\Framework\MockObject\MockObject $authSimpleFactory; - + protected MockObject $oidcConfigurationMock; + protected MockObject $moduleConfigMock; + protected MockObject $authSimpleService; + protected MockObject $authSimpleFactory; + + /** + * @throws \PHPUnit\Framework\MockObject\Exception + */ protected function setUp(): void { $this->permissions = Configuration::loadFromArray( @@ -38,9 +45,8 @@ protected function setUp(): void $this->oidcConfigurationMock = $this->createMock(Configuration::class); - $this->configurationServiceMock = $this->createMock(ConfigurationService::class); - $this->configurationServiceMock->method('getOpenIDConnectConfiguration') - ->willReturn($this->oidcConfigurationMock); + $this->moduleConfigMock = $this->createMock(ModuleConfig::class); + $this->moduleConfigMock->method('config')->willReturn($this->oidcConfigurationMock); $this->authSimpleService = $this->createMock(Simple::class); @@ -51,7 +57,7 @@ protected function setUp(): void protected function prepareMockedInstance(): AuthContextService { return new AuthContextService( - $this->configurationServiceMock, + $this->moduleConfigMock, $this->authSimpleFactory ); } @@ -64,11 +70,12 @@ public function testItIsInitializable(): void ); } + /** + * @throws Exception + */ public function testItReturnsUsername(): void { - $this->oidcConfigurationMock->method('getString') - ->with('useridattr') - ->willReturn('idAttribute'); + $this->moduleConfigMock->method('getUserIdentifierAttribute')->willReturn('idAttribute'); $this->authSimpleService->method('getAttributes')->willReturn(self::AUTHORIZED_USER); $this->assertSame( @@ -80,10 +87,10 @@ public function testItReturnsUsername(): void public function testItThrowsWhenNoUsername(): void { $this->oidcConfigurationMock->method('getOptionalConfigItem') - ->with('permissions', null) + ->with(ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS, null) ->willReturn($this->permissions); $this->oidcConfigurationMock->method('getString') - ->with('useridattr') + ->with(ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE) ->willReturn('attributeNotSet'); $this->authSimpleService->method('getAttributes')->willReturn(self::AUTHORIZED_USER); @@ -91,10 +98,13 @@ public function testItThrowsWhenNoUsername(): void $this->prepareMockedInstance()->getAuthUserId(); } + /** + * @throws \Exception + */ public function testPermissionsOk(): void { $this->oidcConfigurationMock->method('getOptionalConfigItem') - ->with('permissions', null) + ->with(ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS, null) ->willReturn($this->permissions); $this->authSimpleService->method('getAttributes')->willReturn(self::AUTHORIZED_USER); @@ -102,19 +112,25 @@ public function testPermissionsOk(): void $this->expectNotToPerformAssertions(); } + /** + * @throws \Exception + */ public function testItThrowsIfNotAuthorizedForPermission(): void { $this->oidcConfigurationMock->method('getOptionalConfigItem') - ->with('permissions', null) + ->with(ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS, null) ->willReturn($this->permissions); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->prepareMockedInstance()->requirePermission('no-match'); } + /** + * @throws \Exception + */ public function testItThrowsForWrongEntitlements(): void { $this->oidcConfigurationMock->method('getOptionalConfigItem') - ->with('permissions', null) + ->with(ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS, null) ->willReturn($this->permissions); $this->authSimpleService->method('getAttributes') ->willReturn( @@ -124,14 +140,17 @@ public function testItThrowsForWrongEntitlements(): void ] ); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->prepareMockedInstance()->requirePermission('client'); } + /** + * @throws \Exception + */ public function testItThrowsForNotHavingEntitlementAttribute(): void { $this->oidcConfigurationMock->method('getOptionalConfigItem') - ->with('permissions', null) + ->with(ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS, null) ->willReturn($this->permissions); $this->authSimpleService->method('getAttributes') ->willReturn( @@ -140,17 +159,20 @@ public function testItThrowsForNotHavingEntitlementAttribute(): void ] ); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->prepareMockedInstance()->requirePermission('client'); } + /** + * @throws \Exception + */ public function testThrowsForNotHavingEnabledPermissions(): void { $this->oidcConfigurationMock->method('getOptionalConfigItem') - ->with('permissions', null) + ->with(ModuleConfig::OPTION_ADMIN_UI_PERMISSIONS, null) ->willReturn(Configuration::loadFromArray([])); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->prepareMockedInstance()->requirePermission('client'); } } diff --git a/tests/Services/AuthProcServiceTest.php b/tests/src/Services/AuthProcServiceTest.php similarity index 61% rename from tests/Services/AuthProcServiceTest.php rename to tests/src/Services/AuthProcServiceTest.php index 68401a66..72884c83 100644 --- a/tests/Services/AuthProcServiceTest.php +++ b/tests/src/Services/AuthProcServiceTest.php @@ -1,28 +1,42 @@ configurationServiceMock = $this->createMock(ConfigurationService::class); + $this->moduleConfigMock = $this->createMock(ModuleConfig::class); } + /** + * @throws \Exception + */ public function prepareMockedInstance(): AuthProcService { - return new AuthProcService($this->configurationServiceMock); + return new AuthProcService($this->moduleConfigMock); } + /** + * @throws \Exception + */ public function testItIsInitializable(): void { $this->assertInstanceOf( @@ -31,25 +45,31 @@ public function testItIsInitializable(): void ); } + /** + * @throws \Exception + */ public function testItLoadsConfiguredFilters(): void { - $this->configurationServiceMock->method('getAuthProcFilters') - ->willReturn(['\SimpleSAML\Module\core\Auth\Process\AttributeAdd',]); + $this->moduleConfigMock->method('getAuthProcFilters') + ->willReturn(['\\' . AttributeAdd::class,]); $authProcService = $this->prepareMockedInstance(); $this->assertIsArray($authProcService->getLoadedFilters()); $this->assertCount(1, $authProcService->getLoadedFilters()); } + /** + * @throws \Exception + */ public function testItExecutesConfiguredFilters(): void { $sampleFilters = [ 50 => [ - 'class' => '\SimpleSAML\Module\core\Auth\Process\AttributeAdd', + 'class' => '\\' . AttributeAdd::class, 'newKey' => ['newValue'] ], ]; - $this->configurationServiceMock->method('getAuthProcFilters')->willReturn($sampleFilters); + $this->moduleConfigMock->method('getAuthProcFilters')->willReturn($sampleFilters); $state = ['Attributes' => ['existingKey' => ['existingValue']]]; diff --git a/tests/Services/AuthenticationServiceTest.php b/tests/src/Services/AuthenticationServiceTest.php similarity index 77% rename from tests/Services/AuthenticationServiceTest.php rename to tests/src/Services/AuthenticationServiceTest.php index 9daa031e..674e2c96 100644 --- a/tests/Services/AuthenticationServiceTest.php +++ b/tests/src/Services/AuthenticationServiceTest.php @@ -1,25 +1,30 @@ 'https://idp.example.org']; - public const USER_ENTITY_ATTRIBUTES = [ + final public const AUTH_SOURCE = 'auth_source'; + final public const USER_ID_ATTR = 'uid'; + final public const USERNAME = 'username'; + final public const OIDC_OP_METADATA = ['issuer' => 'https://idp.example.org']; + final public const USER_ENTITY_ATTRIBUTES = [ self::USER_ID_ATTR => [self::USERNAME], 'eduPersonTargetedId' => [self::USERNAME], ]; - public const AUTH_DATA = ['Attributes' => self::USER_ENTITY_ATTRIBUTES]; - public const CLIENT_ENTITY = ['id' => 'clientid', 'redirect_uri' => 'https://rp.example.org']; - public const AUTHZ_REQUEST_PARAMS = ['client_id' => 'clientid', 'redirect_uri' => 'https://rp.example.org']; - public const STATE = [ + final public const AUTH_DATA = ['Attributes' => self::USER_ENTITY_ATTRIBUTES]; + final public const CLIENT_ENTITY = ['id' => 'clientid', 'redirect_uri' => 'https://rp.example.org']; + final public const AUTHZ_REQUEST_PARAMS = ['client_id' => 'clientid', 'redirect_uri' => 'https://rp.example.org']; + final public const STATE = [ 'Attributes' => self::AUTH_DATA['Attributes'], 'Oidc' => [ 'OpenIdProviderMetadata' => self::OIDC_OP_METADATA, @@ -47,23 +52,26 @@ class AuthenticationServiceTest extends TestCase ], ]; - public static $uri = 'https://some-server/authorize.php?abc=efg'; - - protected \PHPUnit\Framework\MockObject\MockObject $claimTranslatorExtractorMock; - protected \PHPUnit\Framework\MockObject\MockObject $serverRequestMock; - protected \PHPUnit\Framework\MockObject\MockObject $clientEntityMock; - protected \PHPUnit\Framework\MockObject\MockObject $userRepositoryMock; - protected \PHPUnit\Framework\MockObject\MockObject $authSimpleFactoryMock; - protected \PHPUnit\Framework\MockObject\MockObject $authSimpleMock; - protected \PHPUnit\Framework\MockObject\MockObject $authProcServiceMock; - protected \PHPUnit\Framework\MockObject\MockObject $clientRepositoryMock; - protected \PHPUnit\Framework\MockObject\MockObject $configurationServiceMock; - protected \PHPUnit\Framework\MockObject\MockObject $oidcOpenIdProviderMetadataServiceMock; - protected \PHPUnit\Framework\MockObject\MockObject $sessionServiceMock; - protected \PHPUnit\Framework\MockObject\MockObject $authSourceMock; - protected \PHPUnit\Framework\MockObject\MockObject $sessionMock; - protected \PHPUnit\Framework\MockObject\MockObject $userEntityMock; - + public static string $uri = 'https://some-server/authorize.php?abc=efg'; + + protected MockObject $claimTranslatorExtractorMock; + protected MockObject $serverRequestMock; + protected MockObject $clientEntityMock; + protected MockObject $userRepositoryMock; + protected MockObject $authSimpleFactoryMock; + protected MockObject $authSimpleMock; + protected MockObject $authProcServiceMock; + protected MockObject $clientRepositoryMock; + protected MockObject $moduleConfigMock; + protected MockObject $oidcOpenIdProviderMetadataServiceMock; + protected MockObject $sessionServiceMock; + protected MockObject $authSourceMock; + protected MockObject $sessionMock; + protected MockObject $userEntityMock; + + /** + * @throws \PHPUnit\Framework\MockObject\Exception + */ protected function setUp(): void { $this->serverRequestMock = $this->createMock(ServerRequest::class); @@ -73,8 +81,8 @@ protected function setUp(): void $this->authSimpleMock = $this->createMock(Simple::class); $this->authProcServiceMock = $this->createMock(AuthProcService::class); $this->clientRepositoryMock = $this->createMock(ClientRepository::class); - $this->configurationServiceMock = $this->createMock(ConfigurationService::class); - $this->oidcOpenIdProviderMetadataServiceMock = $this->createMock(OidcOpenIdProviderMetadataService::class); + $this->moduleConfigMock = $this->createMock(ModuleConfig::class); + $this->oidcOpenIdProviderMetadataServiceMock = $this->createMock(OpMetadataService::class); $this->sessionServiceMock = $this->createMock(SessionService::class); $this->claimTranslatorExtractorMock = $this->createMock(ClaimTranslatorExtractor::class); $this->authSourceMock = $this->createMock(Source::class); @@ -96,7 +104,8 @@ protected function setUp(): void $this->oidcOpenIdProviderMetadataServiceMock->method('getMetadata')->willReturn(self::OIDC_OP_METADATA); - $this->configurationServiceMock->method('getAuthProcFilters')->willReturn([]); + $this->moduleConfigMock->method('getAuthProcFilters')->willReturn([]); + $this->moduleConfigMock->method('getUserIdentifierAttribute')->willReturn(self::USER_ID_ATTR); $this->sessionServiceMock->method('getCurrentSession')->willReturn($this->sessionMock); } @@ -111,7 +120,7 @@ public function prepareMockedInstance(): AuthenticationService $this->oidcOpenIdProviderMetadataServiceMock, $this->sessionServiceMock, $this->claimTranslatorExtractorMock, - self::USER_ID_ATTR + $this->moduleConfigMock ); } @@ -123,6 +132,12 @@ public function testItIsInitializable(): void ); } + /** + * @throws AuthSource + * @throws BadRequest + * @throws NotFound + * @throws Exception + */ public function testItCreatesNewUser(): void { $clientId = 'client123'; @@ -159,6 +174,12 @@ public function testItCreatesNewUser(): void ); } + /** + * @throws AuthSource + * @throws BadRequest + * @throws NotFound + * @throws Exception + */ public function testItReturnsAnUser(): void { $clientId = 'client123'; @@ -197,6 +218,11 @@ public function testItReturnsAnUser(): void ); } + /** + * @throws AuthSource + * @throws BadRequest + * @throws NotFound + */ public function testItThrowsIfClaimsNotExist(): void { $this->authSourceMock->method('getAuthId')->willReturn('theAuthId'); diff --git a/tests/Services/IdTokenBuilderTest.php b/tests/src/Services/IdTokenBuilderTest.php similarity index 81% rename from tests/Services/IdTokenBuilderTest.php rename to tests/src/Services/IdTokenBuilderTest.php index f62b09eb..38e5ac28 100644 --- a/tests/Services/IdTokenBuilderTest.php +++ b/tests/src/Services/IdTokenBuilderTest.php @@ -1,5 +1,7 @@ markTestIncomplete(); } diff --git a/tests/Services/JsonWebKeySetServiceTest.php b/tests/src/Services/JsonWebKeySetServiceTest.php similarity index 85% rename from tests/Services/JsonWebKeySetServiceTest.php rename to tests/src/Services/JsonWebKeySetServiceTest.php index 492c86a9..5a7b3ec0 100644 --- a/tests/Services/JsonWebKeySetServiceTest.php +++ b/tests/src/Services/JsonWebKeySetServiceTest.php @@ -1,5 +1,7 @@ assertEquals($JWKSet->all(), $jsonWebKeySetService->keys()); } + /** + * @throws \SimpleSAML\Error\Exception + */ public function testCertificationFileNotFound(): void { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessageMatches('/OpenId Connect certification file does not exists/'); $config = [ @@ -98,6 +102,6 @@ public function testCertificationFileNotFound(): void ]; Configuration::loadFromArray($config, '', 'simplesaml'); - new JsonWebKeySetService(new ConfigurationService()); + new JsonWebKeySetService(new ModuleConfig()); } } diff --git a/tests/Services/JsonWebTokenBuilderServiceTest.php b/tests/src/Services/JsonWebTokenBuilderServiceTest.php similarity index 65% rename from tests/Services/JsonWebTokenBuilderServiceTest.php rename to tests/src/Services/JsonWebTokenBuilderServiceTest.php index b1c940e1..a9413f2b 100644 --- a/tests/Services/JsonWebTokenBuilderServiceTest.php +++ b/tests/src/Services/JsonWebTokenBuilderServiceTest.php @@ -1,7 +1,10 @@ configurationServiceStub = $this->createStub(ConfigurationService::class); - $this->configurationServiceStub->method('getSigner')->willReturn(self::$signerSha256); - $this->configurationServiceStub->method('getPrivateKeyPath')->willReturn(self::$privateKeyPath); - $this->configurationServiceStub->method('getCertPath')->willReturn(self::$publicKeyPath); - $this->configurationServiceStub->method('getSimpleSAMLSelfURLHost')->willReturn(self::$selfUrlHost); + $this->moduleConfigStub = $this->createStub(ModuleConfig::class); + $this->moduleConfigStub->method('getSigner')->willReturn(self::$signerSha256); + $this->moduleConfigStub->method('getPrivateKeyPath')->willReturn(self::$privateKeyPath); + $this->moduleConfigStub->method('getCertPath')->willReturn(self::$publicKeyPath); + $this->moduleConfigStub->method('getSimpleSAMLSelfURLHost')->willReturn(self::$selfUrlHost); } /** @@ -54,7 +60,7 @@ public function setUp(): void */ public function testCanCreateBuilderInstance(): void { - $builderService = new JsonWebTokenBuilderService($this->configurationServiceStub); + $builderService = new JsonWebTokenBuilderService($this->moduleConfigStub); $this->assertInstanceOf( Builder::class, @@ -69,7 +75,7 @@ public function testCanCreateBuilderInstance(): void */ public function testCanGenerateSignedJwtToken(): void { - $builderService = new JsonWebTokenBuilderService($this->configurationServiceStub); + $builderService = new JsonWebTokenBuilderService($this->moduleConfigStub); $tokenBuilder = $builderService->getDefaultJwtTokenBuilder(); $unencryptedToken = $builderService->getSignedJwtTokenFromBuilder($tokenBuilder); @@ -81,12 +87,12 @@ public function testCanGenerateSignedJwtToken(): void $token = $unencryptedToken->toString(); $jwtConfig = Configuration::forAsymmetricSigner( - $this->configurationServiceStub->getSigner(), + $this->moduleConfigStub->getSigner(), InMemory::file( - $this->configurationServiceStub->getPrivateKeyPath(), - $this->configurationServiceStub->getPrivateKeyPassPhrase() ?? '' + $this->moduleConfigStub->getPrivateKeyPath(), + $this->moduleConfigStub->getPrivateKeyPassPhrase() ?? '' ), - InMemory::file($this->configurationServiceStub->getCertPath()) + InMemory::file($this->moduleConfigStub->getCertPath()) ); $parsedToken = $jwtConfig->parser()->parse($token); @@ -96,8 +102,8 @@ public function testCanGenerateSignedJwtToken(): void $parsedToken, new IssuedBy(self::$selfUrlHost), new SignedWith( - $this->configurationServiceStub->getSigner(), - InMemory::file($this->configurationServiceStub->getCertPath()) + $this->moduleConfigStub->getSigner(), + InMemory::file($this->moduleConfigStub->getCertPath()) ) ) ); @@ -110,7 +116,7 @@ public function testCanReturnCurrentSigner(): void { $this->assertSame( self::$signerSha256, - (new JsonWebTokenBuilderService($this->configurationServiceStub))->getSigner() + (new JsonWebTokenBuilderService($this->moduleConfigStub))->getSigner() ); } } diff --git a/tests/Services/LogoutTokenBuilderTest.php b/tests/src/Services/LogoutTokenBuilderTest.php similarity index 69% rename from tests/Services/LogoutTokenBuilderTest.php rename to tests/src/Services/LogoutTokenBuilderTest.php index 4b6f1cd7..01ed7fea 100644 --- a/tests/Services/LogoutTokenBuilderTest.php +++ b/tests/src/Services/LogoutTokenBuilderTest.php @@ -1,7 +1,10 @@ configurationServiceStub = $this->createStub(ConfigurationService::class); - $this->configurationServiceStub->method('getSigner')->willReturn(self::$signerSha256); - $this->configurationServiceStub->method('getPrivateKeyPath')->willReturn(self::$privateKeyPath); - $this->configurationServiceStub->method('getCertPath')->willReturn(self::$publicKeyPath); - $this->configurationServiceStub->method('getSimpleSAMLSelfURLHost')->willReturn(self::$selfUrlHost); + $this->moduleConfigStub = $this->createStub(ModuleConfig::class); + $this->moduleConfigStub->method('getSigner')->willReturn(self::$signerSha256); + $this->moduleConfigStub->method('getPrivateKeyPath')->willReturn(self::$privateKeyPath); + $this->moduleConfigStub->method('getCertPath')->willReturn(self::$publicKeyPath); + $this->moduleConfigStub->method('getSimpleSAMLSelfURLHost')->willReturn(self::$selfUrlHost); $this->relyingPartyAssociationStub = $this->createStub(RelyingPartyAssociationInterface::class); $this->relyingPartyAssociationStub->method('getClientId')->willReturn(self::$clientId); @@ -70,7 +75,7 @@ public function setUp(): void ->method('getBackChannelLogoutUri') ->willReturn(self::$backChannelLogoutUri); - $this->jsonWebTokenBuilderService = new JsonWebTokenBuilderService($this->configurationServiceStub); + $this->jsonWebTokenBuilderService = new JsonWebTokenBuilderService($this->moduleConfigStub); } /** @@ -85,12 +90,12 @@ public function testCanGenerateSignedTokenForRelyingPartyAssociation(): void // Check token validity $jwtConfig = Configuration::forAsymmetricSigner( - $this->configurationServiceStub->getSigner(), + $this->moduleConfigStub->getSigner(), InMemory::file( - $this->configurationServiceStub->getPrivateKeyPath(), - $this->configurationServiceStub->getPrivateKeyPassPhrase() ?? '' + $this->moduleConfigStub->getPrivateKeyPath(), + $this->moduleConfigStub->getPrivateKeyPassPhrase() ?? '' ), - InMemory::file($this->configurationServiceStub->getCertPath()) + InMemory::file($this->moduleConfigStub->getCertPath()) ); $parsedToken = $jwtConfig->parser()->parse($token); @@ -102,8 +107,8 @@ public function testCanGenerateSignedTokenForRelyingPartyAssociation(): void new PermittedFor(self::$clientId), new RelatedTo(self::$userId), new SignedWith( - $this->configurationServiceStub->getSigner(), - InMemory::file($this->configurationServiceStub->getCertPath()) + $this->moduleConfigStub->getSigner(), + InMemory::file($this->moduleConfigStub->getCertPath()) ) ) ); diff --git a/tests/Services/OidcOpenIdProviderMetadataServiceTest.php b/tests/src/Services/OpMetadataServiceTest.php similarity index 67% rename from tests/Services/OidcOpenIdProviderMetadataServiceTest.php rename to tests/src/Services/OpMetadataServiceTest.php index 5888ad98..c8a445b9 100644 --- a/tests/Services/OidcOpenIdProviderMetadataServiceTest.php +++ b/tests/src/Services/OpMetadataServiceTest.php @@ -1,27 +1,34 @@ configurationServiceMock = $this->createMock(ConfigurationService::class); + $this->moduleConfigMock = $this->createMock(ModuleConfig::class); - $this->configurationServiceMock->expects($this->once())->method('getOpenIDScopes') + $this->moduleConfigMock->expects($this->once())->method('getOpenIDScopes') ->willReturn(['openid' => 'openid']); - $this->configurationServiceMock->expects($this->once())->method('getSimpleSAMLSelfURLHost') + $this->moduleConfigMock->expects($this->once())->method('getSimpleSAMLSelfURLHost') ->willReturn('http://localhost'); - $this->configurationServiceMock->method('getOpenIdConnectModuleURL') + $this->moduleConfigMock->method('getOpenIdConnectModuleURL') ->willReturnCallback(function ($path) { $paths = [ 'authorize.php' => 'http://localhost/authorize.php', @@ -33,22 +40,31 @@ public function setUp(): void return $paths[$path] ?? null; }); - $this->configurationServiceMock->method('getAcrValuesSupported')->willReturn(['1']); + $this->moduleConfigMock->method('getAcrValuesSupported')->willReturn(['1']); } - protected function prepareMockedInstance(): OidcOpenIdProviderMetadataService + /** + * @throws \Exception + */ + protected function prepareMockedInstance(): OpMetadataService { - return new OidcOpenIdProviderMetadataService($this->configurationServiceMock); + return new OpMetadataService($this->moduleConfigMock); } + /** + * @throws \Exception + */ public function testItIsInitializable(): void { $this->assertInstanceOf( - OidcOpenIdProviderMetadataService::class, + OpMetadataService::class, $this->prepareMockedInstance() ); } + /** + * @throws \Exception + */ public function testItReturnsExpectedMetadata(): void { $this->assertSame( diff --git a/tests/Services/SessionMessagesServiceTest.php b/tests/src/Services/SessionMessagesServiceTest.php similarity index 82% rename from tests/Services/SessionMessagesServiceTest.php rename to tests/src/Services/SessionMessagesServiceTest.php index df9ce288..0ba4120e 100644 --- a/tests/Services/SessionMessagesServiceTest.php +++ b/tests/src/Services/SessionMessagesServiceTest.php @@ -1,7 +1,11 @@ sessionMock = $this->createMock(Session::class); @@ -31,6 +38,9 @@ public function testItIsInitializable(): void ); } + /** + * @throws \Exception + */ public function testItAddsMessage(): void { $this->sessionMock->expects($this->once()) @@ -54,9 +64,7 @@ public function testItGetsMessages(): void $this->sessionMock->expects($this->exactly(2)) ->method('deleteData') - ->with($this->callback(function ($id) { - return ! in_array($id, ['msg1', 'msg2']); - })); + ->with($this->callback(fn($id) => ! in_array($id, ['msg1', 'msg2']))); $this->prepareMockedInstance()->getMessages(); } diff --git a/tests/Services/SessionServiceTest.php b/tests/src/Services/SessionServiceTest.php similarity index 81% rename from tests/Services/SessionServiceTest.php rename to tests/src/Services/SessionServiceTest.php index 0d132b0e..b56e6510 100644 --- a/tests/Services/SessionServiceTest.php +++ b/tests/src/Services/SessionServiceTest.php @@ -1,5 +1,7 @@ markTestIncomplete(); } diff --git a/tests/src/Stores/Session/LogoutTicketStoreBuilderTest.php b/tests/src/Stores/Session/LogoutTicketStoreBuilderTest.php new file mode 100644 index 00000000..32e71202 --- /dev/null +++ b/tests/src/Stores/Session/LogoutTicketStoreBuilderTest.php @@ -0,0 +1,34 @@ + 'sqlite::memory:', + 'database.username' => null, + 'database.password' => null, + 'database.prefix' => 'phpunit_', + 'database.persistent' => true, + 'database.secondaries' => [], + ]; + + Configuration::loadFromArray($config, '', 'simplesaml'); + $builder = new LogoutTicketStoreBuilder(); + + $this->assertInstanceOf(LogoutTicketStoreInterface::class, $builder->getInstance()); + $this->assertInstanceOf(LogoutTicketStoreInterface::class, $builder::getStaticInstance()); + } +} diff --git a/tests/Store/SessionLogoutTicketStoreDbTest.php b/tests/src/Stores/Session/LogoutTicketStoreDbTest.php similarity index 81% rename from tests/Store/SessionLogoutTicketStoreDbTest.php rename to tests/src/Stores/Session/LogoutTicketStoreDbTest.php index fb113c8c..ddfc9537 100644 --- a/tests/Store/SessionLogoutTicketStoreDbTest.php +++ b/tests/src/Stores/Session/LogoutTicketStoreDbTest.php @@ -1,17 +1,19 @@ add($sid); $allSids = $store->getAll(); @@ -54,7 +56,7 @@ public function testCanDeleteMultipleTickets(): void $sid2 = 'sid2'; $sid3 = 'sid3'; - $store = new SessionLogoutTicketStoreDb(); + $store = new LogoutTicketStoreDb(); $store->add($sid1); $store->add($sid2); $store->add($sid3); @@ -79,7 +81,7 @@ public function testCanDeleteMultipleTickets(): void */ public function testCanDeleteExpiredTickets(): void { - $store = new SessionLogoutTicketStoreDb(null, 0); + $store = new LogoutTicketStoreDb(null, 0); $sid = 'sid123'; $store->add($sid); $this->assertEmpty($store->getAll()); diff --git a/tests/Utils/Checker/RequestRulesManagerTest.php b/tests/src/Utils/Checker/RequestRulesManagerTest.php similarity index 79% rename from tests/Utils/Checker/RequestRulesManagerTest.php rename to tests/src/Utils/Checker/RequestRulesManagerTest.php index a06544a3..a17cde5d 100644 --- a/tests/Utils/Checker/RequestRulesManagerTest.php +++ b/tests/src/Utils/Checker/RequestRulesManagerTest.php @@ -1,8 +1,12 @@ result = $this->createStub(ResultInterface::class); - $this->result->method('getKey')->willReturn($this->key); - $this->result->method('getValue')->willReturn($this->value); + $this->resultStub = $this->createStub(ResultInterface::class); + $this->resultStub->method('getKey')->willReturn($this->key); + $this->resultStub->method('getValue')->willReturn($this->value); - $this->rule = $this->createStub(RequestRuleInterface::class); - $this->rule->method('getKey')->willReturn($this->key); - $this->rule->method('checkRule')->willReturn($this->result); + $this->ruleStub = $this->createStub(RequestRuleInterface::class); + $this->ruleStub->method('getKey')->willReturn($this->key); + $this->ruleStub->method('checkRule')->willReturn($this->resultStub); $this->request = $this->createStub(ServerRequestInterface::class); @@ -47,6 +54,9 @@ public function testConstructWithoutRules(): RequestRulesManager return $requestRulesManager; } + /** + * @throws Exception + */ public function testConstructWithRules(): void { $rules = [$this->createStub(RequestRuleInterface::class)]; @@ -56,13 +66,11 @@ public function testConstructWithRules(): void /** * @depends testConstructWithoutRules * - * @param RequestRulesManager $requestRulesManager - * @return void * @throws OidcServerException */ public function testAddAndCheck(RequestRulesManager $requestRulesManager): void { - $requestRulesManager->add($this->rule); + $requestRulesManager->add($this->ruleStub); $resultBag = $requestRulesManager->check($this->request, [$this->key]); $this->assertInstanceOf(ResultBagInterface::class, $resultBag); @@ -73,8 +81,6 @@ public function testAddAndCheck(RequestRulesManager $requestRulesManager): void /** * @depends testConstructWithoutRules * - * @param RequestRulesManager $requestRulesManager - * @return void * @throws OidcServerException */ public function testCheckWithNonExistingRuleKeyThrows(RequestRulesManager $requestRulesManager): void @@ -86,13 +92,11 @@ public function testCheckWithNonExistingRuleKeyThrows(RequestRulesManager $reque /** * @depends testConstructWithoutRules * - * @param RequestRulesManager $requestRulesManager - * @return void * @throws OidcServerException */ public function testPredefineResult(RequestRulesManager $requestRulesManager): void { - $requestRulesManager->predefineResult($this->result); + $requestRulesManager->predefineResult($this->resultStub); $resultBag = $requestRulesManager->check($this->request, []); $this->assertInstanceOf(ResultBagInterface::class, $resultBag); @@ -102,9 +106,8 @@ public function testPredefineResult(RequestRulesManager $requestRulesManager): v /** * @depends testConstructWithoutRules * - * @param RequestRulesManager $requestRulesManager - * @return void * @throws OidcServerException + * @throws Exception */ public function testSetData(RequestRulesManager $requestRulesManager): void { diff --git a/tests/Utils/Checker/ResultBagTest.php b/tests/src/Utils/Checker/ResultBagTest.php similarity index 87% rename from tests/Utils/Checker/ResultBagTest.php rename to tests/src/Utils/Checker/ResultBagTest.php index 694e33ea..10399e85 100644 --- a/tests/Utils/Checker/ResultBagTest.php +++ b/tests/src/Utils/Checker/ResultBagTest.php @@ -1,7 +1,10 @@ resultBag->add($this->result); $this->assertSame($this->result, $this->resultBag->getOrFail($this->key)); - $this->expectException(\LogicException::class); + $this->expectException(LogicException::class); $this->resultBag->getOrFail('non-existent'); } diff --git a/tests/Utils/Checker/ResultTest.php b/tests/src/Utils/Checker/ResultTest.php similarity index 90% rename from tests/Utils/Checker/ResultTest.php rename to tests/src/Utils/Checker/ResultTest.php index 76fdfba1..80c67cb3 100644 --- a/tests/Utils/Checker/ResultTest.php +++ b/tests/src/Utils/Checker/ResultTest.php @@ -1,5 +1,7 @@ requestStub = $this->createStub(ServerRequestInterface::class); diff --git a/tests/Utils/Checker/Rules/AddClaimsToIdTokenRuleTest.php b/tests/src/Utils/Checker/Rules/AddClaimsToIdTokenRuleTest.php similarity index 85% rename from tests/Utils/Checker/Rules/AddClaimsToIdTokenRuleTest.php rename to tests/src/Utils/Checker/Rules/AddClaimsToIdTokenRuleTest.php index 925e8dd4..54abab16 100644 --- a/tests/Utils/Checker/Rules/AddClaimsToIdTokenRuleTest.php +++ b/tests/src/Utils/Checker/Rules/AddClaimsToIdTokenRuleTest.php @@ -1,7 +1,11 @@ 'client123', @@ -42,8 +46,11 @@ class AddClaimsToIdTokenRuleTest extends TestCase private ResultBag $resultBag; - private $loggerServiceStub; + private Stub $loggerServiceStub; + /** + * @throws Exception + */ protected function setUp(): void { $this->requestStub = $this->createStub(ServerRequestInterface::class); @@ -54,6 +61,7 @@ protected function setUp(): void /** * @dataProvider validResponseTypeProvider + * @throws Throwable */ public function testAddClaimsToIdTokenRuleTest($responseType) { @@ -61,11 +69,11 @@ public function testAddClaimsToIdTokenRuleTest($responseType) $this->resultBag->add(new Result(ResponseTypeRule::class, $responseType)); $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub) ?? - new Result(AddClaimsToIdTokenRule::class, null); + new Result(AddClaimsToIdTokenRule::class, null); $this->assertTrue($result->getValue()); } - public function validResponseTypeProvider(): array + public static function validResponseTypeProvider(): array { return [ ['id_token'], @@ -74,6 +82,7 @@ public function validResponseTypeProvider(): array /** * @dataProvider invalidResponseTypeProvider + * @throws Throwable */ public function testDoNotAddClaimsToIdTokenRuleTest($responseType) { @@ -81,12 +90,12 @@ public function testDoNotAddClaimsToIdTokenRuleTest($responseType) $this->resultBag->add(new Result(ResponseTypeRule::class, $responseType)); $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub) ?? - new Result(AddClaimsToIdTokenRule::class, null); + new Result(AddClaimsToIdTokenRule::class, null); $this->assertFalse($result->getValue()); } - public function invalidResponseTypeProvider(): array + public static function invalidResponseTypeProvider(): array { return [ ['code'], diff --git a/tests/Utils/Checker/Rules/ClientIdRuleTest.php b/tests/src/Utils/Checker/Rules/ClientIdRuleTest.php similarity index 71% rename from tests/Utils/Checker/Rules/ClientIdRuleTest.php rename to tests/src/Utils/Checker/Rules/ClientIdRuleTest.php index cb9d5bbd..9cc47332 100644 --- a/tests/Utils/Checker/Rules/ClientIdRuleTest.php +++ b/tests/src/Utils/Checker/Rules/ClientIdRuleTest.php @@ -1,11 +1,15 @@ clientRepository = $this->createStub(ClientRepositoryInterface::class); + $this->clientRepositoryStub = $this->createStub(ClientRepositoryInterface::class); $this->requestStub = $this->createStub(ServerRequestInterface::class); $this->resultBagStub = $this->createStub(ResultBagInterface::class); $this->loggerServiceStub = $this->createStub(LoggerService::class); @@ -32,48 +39,46 @@ protected function setUp(): void public function testConstruct(): void { - $this->assertInstanceOf(ClientIdRule::class, new ClientIdRule($this->clientRepository)); + $this->assertInstanceOf(ClientIdRule::class, new ClientIdRule($this->clientRepositoryStub)); } public function testCheckRuleEmptyClientIdThrows(): void { $this->requestStub->method('getQueryParams')->willReturn([]); $this->expectException(OidcServerException::class); - (new ClientIdRule($this->clientRepository))->checkRule( + (new ClientIdRule($this->clientRepositoryStub))->checkRule( $this->requestStub, $this->resultBagStub, $this->loggerServiceStub, - [] ); } public function testCheckRuleInvalidClientThrows(): void { $this->requestStub->method('getQueryParams')->willReturn(['client_id' => '123']); - $this->clientRepository->method('getClientEntity')->willReturn('invalid'); + $this->clientRepositoryStub->method('getClientEntity')->willReturn('invalid'); $this->expectException(OidcServerException::class); - (new ClientIdRule($this->clientRepository))->checkRule( + (new ClientIdRule($this->clientRepositoryStub))->checkRule( $this->requestStub, $this->resultBagStub, $this->loggerServiceStub, - [] ); } /** * @throws OidcServerException + * @throws Exception */ public function testCheckRuleForValidClientId(): void { $this->requestStub->method('getQueryParams')->willReturn(['client_id' => '123']); $client = $this->createStub(ClientEntityInterface::class); - $this->clientRepository->method('getClientEntity')->willReturn($client); + $this->clientRepositoryStub->method('getClientEntity')->willReturn($client); - $result = (new ClientIdRule($this->clientRepository))->checkRule( + $result = (new ClientIdRule($this->clientRepositoryStub))->checkRule( $this->requestStub, $this->resultBagStub, $this->loggerServiceStub, - [] ); $this->assertInstanceOf(ResultInterface::class, $result); $this->assertInstanceOf(ClientEntityInterface::class, $result->getValue()); diff --git a/tests/Utils/Checker/Rules/CodeChallengeMethodRuleTest.php b/tests/src/Utils/Checker/Rules/CodeChallengeMethodRuleTest.php similarity index 90% rename from tests/Utils/Checker/Rules/CodeChallengeMethodRuleTest.php rename to tests/src/Utils/Checker/Rules/CodeChallengeMethodRuleTest.php index 4a07f959..c1059028 100644 --- a/tests/Utils/Checker/Rules/CodeChallengeMethodRuleTest.php +++ b/tests/src/Utils/Checker/Rules/CodeChallengeMethodRuleTest.php @@ -1,8 +1,12 @@ rule = new CodeChallengeMethodRule(new CodeChallengeVerifiersRepository()); @@ -47,7 +54,7 @@ public function testCheckRuleRedirectUriDependency(): void { $resultBag = new ResultBag(); $this->expectException(LogicException::class); - $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -59,7 +66,7 @@ public function testCheckRuleStateDependency(): void $resultBag = new ResultBag(); $resultBag->add($this->redirectUriResult); $this->expectException(LogicException::class); - $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -70,7 +77,7 @@ public function testCheckRuleWithInvalidCodeChallengeMethodThrows(): void $resultBag = $this->prepareValidResultBag(); $this->requestStub->method('getQueryParams')->willReturn(['code_challenge_method' => 'invalid']); $this->expectException(OidcServerException::class); - $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -81,7 +88,7 @@ public function testCheckRuleForValidCodeChallengeMethod(): void { $resultBag = $this->prepareValidResultBag(); $this->requestStub->method('getQueryParams')->willReturn(['code_challenge_method' => 'plain']); - $result = $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $result = $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); $this->assertInstanceOf(ResultInterface::class, $result); $this->assertSame('plain', $result->getValue()); diff --git a/tests/Utils/Checker/Rules/CodeChallengeRuleTest.php b/tests/src/Utils/Checker/Rules/CodeChallengeRuleTest.php similarity index 90% rename from tests/Utils/Checker/Rules/CodeChallengeRuleTest.php rename to tests/src/Utils/Checker/Rules/CodeChallengeRuleTest.php index abbc6532..4ac0249a 100644 --- a/tests/Utils/Checker/Rules/CodeChallengeRuleTest.php +++ b/tests/src/Utils/Checker/Rules/CodeChallengeRuleTest.php @@ -1,8 +1,12 @@ rule = new CodeChallengeRule(); @@ -48,7 +55,7 @@ public function testCheckRuleRedirectUriDependency(): void { $resultBag = new ResultBag(); $this->expectException(LogicException::class); - $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -60,7 +67,7 @@ public function testCheckRuleStateDependency(): void $resultBag = new ResultBag(); $resultBag->add($this->redirectUriResult); $this->expectException(LogicException::class); - $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -71,7 +78,7 @@ public function testCheckRuleNoCodeChallengeThrows(): void $resultBag = $this->prepareValidResultBag(); $this->requestStub->method('getQueryParams')->willReturn([]); $this->expectException(OidcServerException::class); - $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -82,7 +89,7 @@ public function testCheckRuleInvalidCodeChallengeThrows(): void $resultBag = $this->prepareValidResultBag(); $this->requestStub->method('getQueryParams')->willReturn(['code_challenge' => 'too-short']); $this->expectException(OidcServerException::class); - $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -93,7 +100,7 @@ public function testCheckRuleForValidCodeChallenge(): void { $resultBag = $this->prepareValidResultBag(); $this->requestStub->method('getQueryParams')->willReturn(['code_challenge' => $this->codeChallenge]); - $result = $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $result = $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); $this->assertInstanceOf(ResultInterface::class, $result); $this->assertSame($this->codeChallenge, $result->getValue()); diff --git a/tests/Utils/Checker/Rules/IdTokenHintRuleTest.php b/tests/src/Utils/Checker/Rules/IdTokenHintRuleTest.php similarity index 71% rename from tests/Utils/Checker/Rules/IdTokenHintRuleTest.php rename to tests/src/Utils/Checker/Rules/IdTokenHintRuleTest.php index a5b8ada7..d71dbc87 100644 --- a/tests/Utils/Checker/Rules/IdTokenHintRuleTest.php +++ b/tests/src/Utils/Checker/Rules/IdTokenHintRuleTest.php @@ -1,5 +1,7 @@ resultBagStub = $this->createStub(ResultBagInterface::class); - $this->configurationServiceStub = $this->createStub(ConfigurationService::class); - $this->configurationServiceStub->method('getSigner')->willReturn(new Sha256()); - $this->configurationServiceStub->method('getSimpleSAMLSelfURLHost')->willReturn(self::$issuer); + $this->moduleConfigStub = $this->createStub(ModuleConfig::class); + $this->moduleConfigStub->method('getSigner')->willReturn(new Sha256()); + $this->moduleConfigStub->method('getSimpleSAMLSelfURLHost')->willReturn(self::$issuer); $this->cryptKeyFactoryStub = $this->createStub(CryptKeyFactory::class); $this->cryptKeyFactoryStub->method('buildPrivateKey')->willReturn(self::$privateKey); $this->cryptKeyFactoryStub->method('buildPublicKey')->willReturn(self::$publicKey); - /** @psalm-suppress ArgumentTypeCoercion */ $this->jwtConfig = Configuration::forAsymmetricSigner( - $this->configurationServiceStub->getSigner(), + $this->moduleConfigStub->getSigner(), InMemory::plainText(self::$privateKey->getKeyContents()), InMemory::plainText(self::$publicKey->getKeyContents()) ); @@ -79,7 +83,7 @@ protected function setUp(): void public function testConstruct(): void { $this->assertInstanceOf(IdTokenHintRule::class, new IdTokenHintRule( - $this->configurationServiceStub, + $this->moduleConfigStub, $this->cryptKeyFactoryStub )); } @@ -90,7 +94,7 @@ public function testConstruct(): void */ public function testCheckRuleIsNullWhenParamNotSet(): void { - $rule = new IdTokenHintRule($this->configurationServiceStub, $this->cryptKeyFactoryStub); + $rule = new IdTokenHintRule($this->moduleConfigStub, $this->cryptKeyFactoryStub); $this->requestStub->method('getMethod')->willReturn(''); $result = $rule->checkRule( $this->requestStub, @@ -108,7 +112,7 @@ public function testCheckRuleThrowsForMalformedIdToken(): void { $this->requestStub->method('getMethod')->willReturn('GET'); $this->requestStub->method('getQueryParams')->willReturn(['id_token_hint' => 'malformed']); - $rule = new IdTokenHintRule($this->configurationServiceStub, $this->cryptKeyFactoryStub); + $rule = new IdTokenHintRule($this->moduleConfigStub, $this->cryptKeyFactoryStub); $this->expectException(Throwable::class); $rule->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub); } @@ -120,14 +124,14 @@ public function testCheckRuleThrowsForIdTokenWithInvalidSignature(): void { $this->requestStub->method('getMethod')->willReturn('GET'); $invalidSignatureJwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUub3JnIiwic3ViIjo' . - 'iMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.JGJ_KSiXiRsgVc5nYFTSqbaeeM3UA5DGnOTaz3' . - 'UqbyHM0ogO3rq_-8FwLRzGk-7942U6rQ1ARziLsYYsUtH7yaUTWi6xSvh_mJQuF8hl_X3OghJWeXWms42OjAkHXtB-H7LQ_bEQNV' . - 'nF8XYLsq06MoHeHxAnDkVpVcZyDrmhauAqV1PTWsC9FMMKaxfoVsIbeQ0-PV_gAgzSK5-T0bliXPUdWFjvPXJ775jqqy4ZyNJYh' . - '1_rZ1WyOrJu7AHkT9R4FNQNCw40BRzfI3S_OYBNirKAh5G0sctNwEEaJL_a3lEwKYSC-d_sZ6WBvFP8B138b7T6nPzI71tvfXE' . - 'Ru7Q7rA'; + 'iMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.JGJ_KSiXiRsgVc5nYFTSqbaeeM3UA5DGnOTaz3' . + 'UqbyHM0ogO3rq_-8FwLRzGk-7942U6rQ1ARziLsYYsUtH7yaUTWi6xSvh_mJQuF8hl_X3OghJWeXWms42OjAkHXtB-H7LQ_bEQNV' . + 'nF8XYLsq06MoHeHxAnDkVpVcZyDrmhauAqV1PTWsC9FMMKaxfoVsIbeQ0-PV_gAgzSK5-T0bliXPUdWFjvPXJ775jqqy4ZyNJYh' . + '1_rZ1WyOrJu7AHkT9R4FNQNCw40BRzfI3S_OYBNirKAh5G0sctNwEEaJL_a3lEwKYSC-d_sZ6WBvFP8B138b7T6nPzI71tvfXE' . + 'Ru7Q7rA'; $this->requestStub->method('getQueryParams')->willReturn(['id_token_hint' => $invalidSignatureJwt]); - $rule = new IdTokenHintRule($this->configurationServiceStub, $this->cryptKeyFactoryStub); + $rule = new IdTokenHintRule($this->moduleConfigStub, $this->cryptKeyFactoryStub); $this->expectException(Throwable::class); $rule->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub); } @@ -140,14 +144,13 @@ public function testCheckRuleThrowsForIdTokenWithInvalidIssuer(): void { $this->requestStub->method('getMethod')->willReturn('GET'); - /** @psalm-suppress ArgumentTypeCoercion */ $invalidIssuerJwt = $this->jwtConfig->builder()->issuedBy('invalid')->getToken( - $this->configurationServiceStub->getSigner(), + $this->moduleConfigStub->getSigner(), InMemory::plainText(self::$privateKey->getKeyContents()) )->toString(); $this->requestStub->method('getQueryParams')->willReturn(['id_token_hint' => $invalidIssuerJwt]); - $rule = new IdTokenHintRule($this->configurationServiceStub, $this->cryptKeyFactoryStub); + $rule = new IdTokenHintRule($this->moduleConfigStub, $this->cryptKeyFactoryStub); $this->expectException(Throwable::class); $rule->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub); } @@ -161,16 +164,15 @@ public function testCheckRulePassesForValidIdToken(): void { $this->requestStub->method('getMethod')->willReturn('GET'); - /** @psalm-suppress ArgumentTypeCoercion */ $idToken = $this->jwtConfig->builder()->issuedBy(self::$issuer)->getToken( - $this->configurationServiceStub->getSigner(), + $this->moduleConfigStub->getSigner(), InMemory::plainText(self::$privateKey->getKeyContents()) )->toString(); $this->requestStub->method('getQueryParams')->willReturn(['id_token_hint' => $idToken]); - $rule = new IdTokenHintRule($this->configurationServiceStub, $this->cryptKeyFactoryStub); + $rule = new IdTokenHintRule($this->moduleConfigStub, $this->cryptKeyFactoryStub); $result = $rule->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - new Result(IdTokenHintRule::class); + new Result(IdTokenHintRule::class); $this->assertInstanceOf(UnencryptedToken::class, $result->getValue()); } diff --git a/tests/Utils/Checker/Rules/PostLogoutRedirectUriRuleTest.php b/tests/src/Utils/Checker/Rules/PostLogoutRedirectUriRuleTest.php similarity index 80% rename from tests/Utils/Checker/Rules/PostLogoutRedirectUriRuleTest.php rename to tests/src/Utils/Checker/Rules/PostLogoutRedirectUriRuleTest.php index d73557ac..145390e0 100644 --- a/tests/Utils/Checker/Rules/PostLogoutRedirectUriRuleTest.php +++ b/tests/src/Utils/Checker/Rules/PostLogoutRedirectUriRuleTest.php @@ -1,13 +1,18 @@ clientRepository = $this->createStub(ClientRepository::class); + $this->clientRepositoryStub = $this->createStub(ClientRepository::class); $this->requestStub = $this->createStub(ServerRequestInterface::class); $this->requestStub->method('getMethod')->willReturn('GET'); $this->resultBagStub = $this->createStub(ResultBagInterface::class); $this->clientStub = $this->createStub(ClientEntityInterface::class); - /** @psalm-suppress ArgumentTypeCoercion */ $this->jwtConfig = Configuration::forAsymmetricSigner( new Sha256(), InMemory::plainText(self::$privateKey->getKeyContents()), @@ -74,9 +81,9 @@ protected function setUp(): void */ public function testCheckRuleReturnsNullIfNoParamSet(): void { - $result = (new PostLogoutRedirectUriRule($this->clientRepository)) + $result = (new PostLogoutRedirectUriRule($this->clientRepositoryStub)) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - (new Result(PostLogoutRedirectUriRule::class)); + (new Result(PostLogoutRedirectUriRule::class)); $this->assertNull($result->getValue()); } @@ -91,9 +98,9 @@ public function testCheckRuleThrowsWhenIdTokenHintNotAvailable(): void $this->expectException(Throwable::class); - (new PostLogoutRedirectUriRule($this->clientRepository)) + (new PostLogoutRedirectUriRule($this->clientRepositoryStub)) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - (new Result(PostLogoutRedirectUriRule::class)); + (new Result(PostLogoutRedirectUriRule::class)); } /** @@ -104,7 +111,6 @@ public function testCheckRuleThrowsWhenAudClaimNotValid(): void $this->requestStub->method('getQueryParams') ->willReturn(['post_logout_redirect_uri' => self::$postLogoutRedirectUri]); - /** @psalm-suppress ArgumentTypeCoercion */ $jwt = $this->jwtConfig->builder()->issuedBy(self::$issuer) ->getToken( new Sha256(), @@ -119,9 +125,9 @@ public function testCheckRuleThrowsWhenAudClaimNotValid(): void $this->expectException(Throwable::class); - (new PostLogoutRedirectUriRule($this->clientRepository)) + (new PostLogoutRedirectUriRule($this->clientRepositoryStub)) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - (new Result(PostLogoutRedirectUriRule::class)); + (new Result(PostLogoutRedirectUriRule::class)); } /** @@ -132,7 +138,6 @@ public function testCheckRuleThrowsWhenClientNotFound(): void $this->requestStub->method('getQueryParams') ->willReturn(['post_logout_redirect_uri' => self::$postLogoutRedirectUri]); - /** @psalm-suppress ArgumentTypeCoercion */ $jwt = $this->jwtConfig->builder() ->issuedBy(self::$issuer) ->permittedFor('invalid-client-id') @@ -141,7 +146,7 @@ public function testCheckRuleThrowsWhenClientNotFound(): void InMemory::plainText(self::$privateKey->getKeyContents()) ); - $this->clientRepository->method('findById')->willReturn(null); + $this->clientRepositoryStub->method('findById')->willReturn(null); $this->resultBagStub->method('getOrFail')->willReturnOnConsecutiveCalls( new Result(StateRule::class), @@ -150,9 +155,9 @@ public function testCheckRuleThrowsWhenClientNotFound(): void $this->expectException(Throwable::class); - (new PostLogoutRedirectUriRule($this->clientRepository)) + (new PostLogoutRedirectUriRule($this->clientRepositoryStub)) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - (new Result(PostLogoutRedirectUriRule::class)); + (new Result(PostLogoutRedirectUriRule::class)); } /** @@ -163,7 +168,6 @@ public function testCheckRuleThrowsWhenPostLogoutRegisteredUriNotRegistered(): v $this->requestStub->method('getQueryParams') ->willReturn(['post_logout_redirect_uri' => self::$postLogoutRedirectUri]); - /** @psalm-suppress ArgumentTypeCoercion */ $jwt = $this->jwtConfig->builder() ->issuedBy(self::$issuer) ->permittedFor('client-id') @@ -176,7 +180,7 @@ public function testCheckRuleThrowsWhenPostLogoutRegisteredUriNotRegistered(): v 'https://some-other-uri' ]); - $this->clientRepository->method('findById')->willReturn($this->clientStub); + $this->clientRepositoryStub->method('findById')->willReturn($this->clientStub); $this->resultBagStub->method('getOrFail')->willReturnOnConsecutiveCalls( new Result(StateRule::class), @@ -185,9 +189,9 @@ public function testCheckRuleThrowsWhenPostLogoutRegisteredUriNotRegistered(): v $this->expectException(Throwable::class); - (new PostLogoutRedirectUriRule($this->clientRepository)) + (new PostLogoutRedirectUriRule($this->clientRepositoryStub)) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - (new Result(PostLogoutRedirectUriRule::class)); + (new Result(PostLogoutRedirectUriRule::class)); } /** @@ -199,7 +203,6 @@ public function testCheckRuleReturnsForRegisteredPostLogoutRedirectUri(): void $this->requestStub->method('getQueryParams') ->willReturn(['post_logout_redirect_uri' => self::$postLogoutRedirectUri]); - /** @psalm-suppress ArgumentTypeCoercion */ $jwt = $this->jwtConfig->builder() ->issuedBy(self::$issuer) ->permittedFor('client-id') @@ -212,16 +215,16 @@ public function testCheckRuleReturnsForRegisteredPostLogoutRedirectUri(): void self::$postLogoutRedirectUri ]); - $this->clientRepository->method('findById')->willReturn($this->clientStub); + $this->clientRepositoryStub->method('findById')->willReturn($this->clientStub); $this->resultBagStub->method('getOrFail')->willReturnOnConsecutiveCalls( new Result(StateRule::class), new Result(IdTokenHintRule::class, $jwt) ); - $result = (new PostLogoutRedirectUriRule($this->clientRepository)) + $result = (new PostLogoutRedirectUriRule($this->clientRepositoryStub)) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - (new Result(PostLogoutRedirectUriRule::class)); + (new Result(PostLogoutRedirectUriRule::class)); $this->assertEquals(self::$postLogoutRedirectUri, $result->getValue()); } diff --git a/tests/Utils/Checker/Rules/RedirectUriRuleTest.php b/tests/src/Utils/Checker/Rules/RedirectUriRuleTest.php similarity index 69% rename from tests/Utils/Checker/Rules/RedirectUriRuleTest.php rename to tests/src/Utils/Checker/Rules/RedirectUriRuleTest.php index 68de3f23..34a94190 100644 --- a/tests/Utils/Checker/Rules/RedirectUriRuleTest.php +++ b/tests/src/Utils/Checker/Rules/RedirectUriRuleTest.php @@ -1,11 +1,15 @@ rule = new RedirectUriRule(); $this->resultBag = new ResultBag(); $this->clientStub = $this->createStub(ClientEntityInterface::class); - $this->request = $this->createStub(ServerRequestInterface::class); + $this->requestStub = $this->createStub(ServerRequestInterface::class); $this->loggerServiceStub = $this->createStub(LoggerService::class); } @@ -44,7 +51,7 @@ protected function setUp(): void public function testCheckRuleClientIdDependancy(): void { $this->expectException(LogicException::class); - $this->rule->checkRule($this->request, $this->resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub); } /** @@ -55,7 +62,7 @@ public function testCheckRuleWithInvalidClientDependancy(): void { $this->resultBag->add(new Result(ClientIdRule::class, 'invalid')); $this->expectException(LogicException::class); - $this->rule->checkRule($this->request, $this->resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub); } /** @@ -63,11 +70,11 @@ public function testCheckRuleWithInvalidClientDependancy(): void */ public function testCheckRuleRedirectUriNotSetThrows(): void { - $this->request->method('getQueryParams')->willReturn([]); + $this->requestStub->method('getQueryParams')->willReturn([]); $resultBag = $this->prepareValidResultBag(); $this->expectException(OidcServerException::class); - $this->rule->checkRule($this->request, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -75,11 +82,11 @@ public function testCheckRuleRedirectUriNotSetThrows(): void */ public function testCheckRuleDifferentClientRedirectUriThrows(): void { - $this->request->method('getQueryParams')->willReturn(['redirect_uri' => 'invalid']); + $this->requestStub->method('getQueryParams')->willReturn(['redirect_uri' => 'invalid']); $resultBag = $this->prepareValidResultBag(); $this->expectException(OidcServerException::class); - $this->rule->checkRule($this->request, $resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -87,13 +94,13 @@ public function testCheckRuleDifferentClientRedirectUriThrows(): void */ public function testCheckRuleDifferentClientRedirectUriArrayThrows(): void { - $this->request->method('getQueryParams')->willReturn(['redirect_uri' => 'invalid']); + $this->requestStub->method('getQueryParams')->willReturn(['redirect_uri' => 'invalid']); $this->clientStub->method('getRedirectUri')->willReturn([$this->redirectUri]); $this->resultBag->add(new Result(ClientIdRule::class, $this->clientStub)); $this->expectException(OidcServerException::class); - $this->rule->checkRule($this->request, $this->resultBag, $this->loggerServiceStub, []); + $this->rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub); } /** @@ -102,10 +109,10 @@ public function testCheckRuleDifferentClientRedirectUriArrayThrows(): void */ public function testCheckRuleWithValidRedirectUri(): void { - $this->request->method('getQueryParams')->willReturn(['redirect_uri' => $this->redirectUri]); + $this->requestStub->method('getQueryParams')->willReturn(['redirect_uri' => $this->redirectUri]); $resultBag = $this->prepareValidResultBag(); - $result = $this->rule->checkRule($this->request, $resultBag, $this->loggerServiceStub, []); + $result = $this->rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); $this->assertInstanceOf(ResultInterface::class, $result); $this->assertSame($this->redirectUri, $result->getValue()); diff --git a/tests/Utils/Checker/Rules/RequestedClaimsRuleTest.php b/tests/src/Utils/Checker/Rules/RequestedClaimsRuleTest.php similarity index 78% rename from tests/Utils/Checker/Rules/RequestedClaimsRuleTest.php rename to tests/src/Utils/Checker/Rules/RequestedClaimsRuleTest.php index 0724b1b0..7b2218b3 100644 --- a/tests/Utils/Checker/Rules/RequestedClaimsRuleTest.php +++ b/tests/src/Utils/Checker/Rules/RequestedClaimsRuleTest.php @@ -1,17 +1,20 @@ resultBag = new ResultBag(); $this->clientStub = $this->createStub(ClientEntityInterface::class); - $this->request = $this->createStub(ServerRequestInterface::class); + $this->requestStub = $this->createStub(ServerRequestInterface::class); $this->clientStub->method('getScopes')->willReturn(['openid', 'profile', 'email']); $this->resultBag->add(new Result(ClientIdRule::class, $this->clientStub)); $this->loggerServiceStub = $this->createStub(LoggerService::class); } /** - * @throws InvalidArgumentException * @throws Throwable */ public function testNoRequestedClaims(): void { $rule = new RequestedClaimsRule(new ClaimTranslatorExtractor(self::$userIdAttr)); - $result = $rule->checkRule($this->request, $this->resultBag, $this->loggerServiceStub, []); + $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub); $this->assertNull($result); } /** - * @throws InvalidArgumentException * @throws Throwable */ public function testWithClaims(): void @@ -75,20 +79,19 @@ public function testWithClaims(): void // Add some claims the client is not authorized for $requestedClaims['userinfo']['someClaim'] = null; $requestedClaims['id_token']['secret_password'] = null; - $this->request->method('getQueryParams')->willReturn([ + $this->requestStub->method('getQueryParams')->willReturn([ 'claims' => json_encode($requestedClaims), 'client_id' => 'abc' ]); $rule = new RequestedClaimsRule(new ClaimTranslatorExtractor(self::$userIdAttr)); - $result = $rule->checkRule($this->request, $this->resultBag, $this->loggerServiceStub, []); + $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub); $this->assertNotNull($result); $this->assertEquals($expectedClaims, $result->getValue()); } /** - * @throws InvalidArgumentException * @throws Throwable */ public function testOnlyWithNonStandardClaimRequest(): void @@ -99,13 +102,13 @@ public function testOnlyWithNonStandardClaimRequest(): void ] ]; $requestedClaims = $expectedClaims; - $this->request->method('getQueryParams')->willReturn([ + $this->requestStub->method('getQueryParams')->willReturn([ 'claims' => json_encode($requestedClaims), 'client_id' => 'abc' ]); $rule = new RequestedClaimsRule(new ClaimTranslatorExtractor(self::$userIdAttr)); - $result = $rule->checkRule($this->request, $this->resultBag, $this->loggerServiceStub, []); + $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub); $this->assertNotNull($result); $this->assertEquals($expectedClaims, $result->getValue()); } diff --git a/tests/Utils/Checker/Rules/RequiredNonceRuleTest.php b/tests/src/Utils/Checker/Rules/RequiredNonceRuleTest.php similarity index 89% rename from tests/Utils/Checker/Rules/RequiredNonceRuleTest.php rename to tests/src/Utils/Checker/Rules/RequiredNonceRuleTest.php index b077422c..8b117832 100644 --- a/tests/Utils/Checker/Rules/RequiredNonceRuleTest.php +++ b/tests/src/Utils/Checker/Rules/RequiredNonceRuleTest.php @@ -1,8 +1,12 @@ 'nonce123', 'state' => 'state123', ]; - protected $loggerServiceStub; + protected Stub $loggerServiceStub; + /** + * @throws Exception + */ protected function setUp(): void { $this->redirectUriResult = new Result(RedirectUriRule::class, 'https://some-uri.org'); @@ -54,7 +61,7 @@ public function testCheckRuleRedirectUriDependency(): void $rule = new RequiredNonceRule(); $resultBag = new ResultBag(); $this->expectException(LogicException::class); - $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -67,7 +74,7 @@ public function testCheckRuleStateDependency(): void $resultBag = new ResultBag(); $resultBag->add($this->redirectUriResult); $this->expectException(LogicException::class); - $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -80,8 +87,8 @@ public function testCheckRulePassesWhenNonceIsPresent() $this->requestStub->method('getQueryParams')->willReturn($this->requestQueryParams); - $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub, []) ?? - new Result(RequiredNonceRule::class, null); + $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub) ?? + new Result(RequiredNonceRule::class, null); $this->assertEquals($this->requestQueryParams['nonce'], $result->getValue()); } @@ -100,6 +107,6 @@ public function testCheckRuleThrowsWhenNonceIsNotPresent() $this->expectException(OidcServerException::class); - $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub, []); + $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub); } } diff --git a/tests/Utils/Checker/Rules/RequiredOpenIdScopeRuleTest.php b/tests/src/Utils/Checker/Rules/RequiredOpenIdScopeRuleTest.php similarity index 89% rename from tests/Utils/Checker/Rules/RequiredOpenIdScopeRuleTest.php rename to tests/src/Utils/Checker/Rules/RequiredOpenIdScopeRuleTest.php index 7497a89f..22a56b26 100644 --- a/tests/Utils/Checker/Rules/RequiredOpenIdScopeRuleTest.php +++ b/tests/src/Utils/Checker/Rules/RequiredOpenIdScopeRuleTest.php @@ -1,11 +1,15 @@ redirectUriResult = new Result(RedirectUriRule::class, 'https://some-uri.org'); @@ -53,7 +60,7 @@ public function testCheckRuleRedirectUriDependency(): void $rule = new RequiredOpenIdScopeRule(); $resultBag = new ResultBag(); $this->expectException(LogicException::class); - $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -66,7 +73,7 @@ public function testCheckRuleStateDependency(): void $resultBag = new ResultBag(); $resultBag->add($this->redirectUriResult); $this->expectException(LogicException::class); - $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } /** @@ -81,8 +88,8 @@ public function testCheckRulePassesWhenOpenIdScopeIsPresent() $resultBag->add($this->stateResult); $resultBag->add($this->scopeResult); - $result = $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []) ?? - new Result(RequiredOpenIdScopeRule::class, null); + $result = $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub) ?? + new Result(RequiredOpenIdScopeRule::class, null); $this->assertTrue($result->getValue()); } @@ -103,6 +110,6 @@ public function testCheckRuleThrowsWhenOpenIdScopeIsNotPresent() $this->expectException(OidcServerException::class); - $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub, []); + $rule->checkRule($this->requestStub, $resultBag, $this->loggerServiceStub); } } diff --git a/tests/Utils/Checker/Rules/ResponseTypeRuleTest.php b/tests/src/Utils/Checker/Rules/ResponseTypeRuleTest.php similarity index 87% rename from tests/Utils/Checker/Rules/ResponseTypeRuleTest.php rename to tests/src/Utils/Checker/Rules/ResponseTypeRuleTest.php index 21456c11..9838ac13 100644 --- a/tests/Utils/Checker/Rules/ResponseTypeRuleTest.php +++ b/tests/src/Utils/Checker/Rules/ResponseTypeRuleTest.php @@ -1,7 +1,11 @@ 'client123', @@ -41,8 +45,11 @@ class ResponseTypeRuleTest extends TestCase */ private ResultBag $resultBag; - protected $loggerServiceStub; + protected Stub $loggerServiceStub; + /** + * @throws Exception + */ protected function setUp(): void { $this->requestStub = $this->createStub(ServerRequestInterface::class); @@ -62,11 +69,11 @@ public function testResponseTypeRuleTest($responseType) $this->requestParams['response_type'] = $responseType; $this->requestStub->method('getQueryParams')->willReturn($this->requestParams); $result = $rule->checkRule($this->requestStub, $this->resultBag, $this->loggerServiceStub) ?? - new Result(ResponseTypeRule::class, null); + new Result(ResponseTypeRule::class, null); $this->assertSame($responseType, $result->getValue()); } - public function validResponseTypeProvider(): array + public static function validResponseTypeProvider(): array { return [ ['id_token'], diff --git a/tests/Utils/Checker/Rules/ScopeOfflineAccessRuleTest.php b/tests/src/Utils/Checker/Rules/ScopeOfflineAccessRuleTest.php similarity index 72% rename from tests/Utils/Checker/Rules/ScopeOfflineAccessRuleTest.php rename to tests/src/Utils/Checker/Rules/ScopeOfflineAccessRuleTest.php index 266c0539..cf56ff28 100644 --- a/tests/Utils/Checker/Rules/ScopeOfflineAccessRuleTest.php +++ b/tests/src/Utils/Checker/Rules/ScopeOfflineAccessRuleTest.php @@ -1,74 +1,46 @@ serverRequestStub = $this->createStub(ServerRequestInterface::class); @@ -90,7 +62,7 @@ protected function setUp(): void $this->clientResultStub = $this->createStub(ResultInterface::class); $this->validScopesResultStub = $this->createStub(ResultInterface::class); - $this->configurationServiceStub = $this->createStub(ConfigurationService::class); + $this->moduleConfigStub = $this->createStub(ModuleConfig::class); $this->openIdConfigurationStub = $this->createStub(Configuration::class); } @@ -98,10 +70,14 @@ public function testCanCreateInstance(): void { $this->assertInstanceOf( ScopeOfflineAccessRule::class, - new ScopeOfflineAccessRule($this->configurationServiceStub) + new ScopeOfflineAccessRule() ); } + /** + * @throws Throwable + * @throws OidcServerException + */ public function testReturnsFalseWhenOfflineAccessScopeNotPresent(): void { $this->clientStub->method('getScopes')->willReturn(['openid']); @@ -118,16 +94,19 @@ public function testReturnsFalseWhenOfflineAccessScopeNotPresent(): void ); $this->openIdConfigurationStub->method('getBoolean')->willReturn(false); - $this->configurationServiceStub->method('getOpenIDConnectConfiguration') + $this->moduleConfigStub->method('config') ->willReturn($this->openIdConfigurationStub); - $result = (new ScopeOfflineAccessRule($this->configurationServiceStub)) + $result = (new ScopeOfflineAccessRule()) ->checkRule($this->serverRequestStub, $this->resultBagMock, $this->loggerServiceMock); $this->assertNotNull($result); $this->assertFalse($result->getValue()); } + /** + * @throws Throwable + */ public function testThrowsWhenClientDoesntHaveOfflineAccessScopeRegistered(): void { $this->clientStub->method('getScopes')->willReturn(['openid']); @@ -145,15 +124,19 @@ public function testThrowsWhenClientDoesntHaveOfflineAccessScopeRegistered(): vo ); $this->openIdConfigurationStub->method('getBoolean')->willReturn(false); - $this->configurationServiceStub->method('getOpenIDConnectConfiguration') + $this->moduleConfigStub->method('config') ->willReturn($this->openIdConfigurationStub); $this->expectException(OidcServerException::class); - (new ScopeOfflineAccessRule($this->configurationServiceStub)) + (new ScopeOfflineAccessRule()) ->checkRule($this->serverRequestStub, $this->resultBagMock, $this->loggerServiceMock); } + /** + * @throws Throwable + * @throws OidcServerException + */ public function testReturnsTrueWhenClientDoesHaveOfflineAccessScopeRegistered(): void { $this->clientStub->method('getScopes')->willReturn(['openid', 'offline_access']); @@ -171,10 +154,10 @@ public function testReturnsTrueWhenClientDoesHaveOfflineAccessScopeRegistered(): ); $this->openIdConfigurationStub->method('getBoolean')->willReturn(false); - $this->configurationServiceStub->method('getOpenIDConnectConfiguration') + $this->moduleConfigStub->method('config') ->willReturn($this->openIdConfigurationStub); - $result = (new ScopeOfflineAccessRule($this->configurationServiceStub)) + $result = (new ScopeOfflineAccessRule()) ->checkRule($this->serverRequestStub, $this->resultBagMock, $this->loggerServiceMock); $this->assertNotNull($result); diff --git a/tests/Utils/Checker/Rules/ScopeRuleTest.php b/tests/src/Utils/Checker/Rules/ScopeRuleTest.php similarity index 91% rename from tests/Utils/Checker/Rules/ScopeRuleTest.php rename to tests/src/Utils/Checker/Rules/ScopeRuleTest.php index 72015a19..134a74ec 100644 --- a/tests/Utils/Checker/Rules/ScopeRuleTest.php +++ b/tests/src/Utils/Checker/Rules/ScopeRuleTest.php @@ -1,12 +1,17 @@ '', 'scope_delimiter_string' => ' ', @@ -36,10 +41,13 @@ class ScopeRuleTest extends TestCase protected Result $redirectUriResult; protected Result $stateResult; - protected $requestStub; + protected Stub $requestStub; - protected $loggerServiceStub; + protected Stub $loggerServiceStub; + /** + * @throws Exception + */ protected function setUp(): void { $this->scopeRepositoryStub = $this->createStub(ScopeRepositoryInterface::class); @@ -137,7 +145,7 @@ protected function prepareValidResultBag(): ResultBag return $resultBag; } - protected function prepareValidScopeRepositoryStub() + protected function prepareValidScopeRepositoryStub(): InvocationStubber { return $this->scopeRepositoryStub ->method('getScopeEntityByIdentifier') diff --git a/tests/Utils/Checker/Rules/StateRuleTest.php b/tests/src/Utils/Checker/Rules/StateRuleTest.php similarity index 92% rename from tests/Utils/Checker/Rules/StateRuleTest.php rename to tests/src/Utils/Checker/Rules/StateRuleTest.php index e9f3a3e8..dff253b8 100644 --- a/tests/Utils/Checker/Rules/StateRuleTest.php +++ b/tests/src/Utils/Checker/Rules/StateRuleTest.php @@ -1,7 +1,11 @@ loggerServiceStub = $this->createStub(LoggerService::class); @@ -31,6 +38,7 @@ public function testGetKey(): void /** * @throws OidcServerException + * @throws Exception */ public function testCheckRuleGetMethod(): void { @@ -52,6 +60,7 @@ public function testCheckRuleGetMethod(): void /** * @throws OidcServerException + * @throws Exception */ public function testCheckRulePostMethod(): void { @@ -73,6 +82,7 @@ public function testCheckRulePostMethod(): void /** * @throws OidcServerException + * @throws Exception */ public function testCheckRuleReturnsNullWhenMethodNotAllowed(): void { @@ -85,7 +95,7 @@ public function testCheckRuleReturnsNullWhenMethodNotAllowed(): void $resultBag = new ResultBag(); $rule = new StateRule(); - $result = $rule->checkRule($request, $resultBag, $this->loggerServiceStub, [], false, ['GET']); + $result = $rule->checkRule($request, $resultBag, $this->loggerServiceStub); $this->assertInstanceOf(ResultInterface::class, $result); $this->assertNull($result->getValue()); @@ -93,6 +103,7 @@ public function testCheckRuleReturnsNullWhenMethodNotAllowed(): void /** * @throws OidcServerException + * @throws Exception */ public function testCheckRuleReturnsNullWhenMethodNotSupported(): void { diff --git a/tests/Utils/Checker/Rules/UiLocalesRuleTest.php b/tests/src/Utils/Checker/Rules/UiLocalesRuleTest.php similarity index 83% rename from tests/Utils/Checker/Rules/UiLocalesRuleTest.php rename to tests/src/Utils/Checker/Rules/UiLocalesRuleTest.php index 3c3a7743..9a5fe1da 100644 --- a/tests/Utils/Checker/Rules/UiLocalesRuleTest.php +++ b/tests/src/Utils/Checker/Rules/UiLocalesRuleTest.php @@ -1,7 +1,11 @@ requestStub = $this->createStub(ServerRequestInterface::class); @@ -37,7 +44,7 @@ public function testCheckRuleReturnsResultWhenParamSet() $result = (new UiLocalesRule()) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - new Result(UiLocalesRule::class); + new Result(UiLocalesRule::class); $this->assertEquals('en', $result->getValue()); } @@ -51,7 +58,7 @@ public function testCheckRuleReturnsNullWhenParamNotSet() $result = (new UiLocalesRule()) ->checkRule($this->requestStub, $this->resultBagStub, $this->loggerServiceStub) ?? - new Result(UiLocalesRule::class); + new Result(UiLocalesRule::class); $this->assertNull($result->getValue()); } diff --git a/tests/ClaimTranslatorExtractorTest.php b/tests/src/Utils/ClaimTranslatorExtractorTest.php similarity index 93% rename from tests/ClaimTranslatorExtractorTest.php rename to tests/src/Utils/ClaimTranslatorExtractorTest.php index 0e472d7b..536d7317 100644 --- a/tests/ClaimTranslatorExtractorTest.php +++ b/tests/src/Utils/ClaimTranslatorExtractorTest.php @@ -1,22 +1,25 @@ scopeEntityOpenId = $this->createStub(ScopeEntityInterface::class); - $this->scopeEntityOpenId->method('getIdentifier')->willReturn('openid'); - $this->scopeEntityProfile = $this->createStub(ScopeEntityInterface::class); - $this->scopeEntityProfile->method('getIdentifier')->willReturn('profile'); + $this->scopeEntityOpenIdStub = $this->createStub(ScopeEntityInterface::class); + $this->scopeEntityOpenIdStub->method('getIdentifier')->willReturn('openid'); + $this->scopeEntityProfileStub = $this->createStub(ScopeEntityInterface::class); + $this->scopeEntityProfileStub->method('getIdentifier')->willReturn('profile'); $this->scopeEntitiesArray = [ - $this->scopeEntityOpenId, - $this->scopeEntityProfile + $this->scopeEntityOpenIdStub, + $this->scopeEntityProfileStub ]; } + /** + * @throws OidcServerException + */ public function testCanCheckScopeExistence(): void { $this->assertTrue(ScopeHelper::scopeExists($this->scopeEntitiesArray, 'openid')); diff --git a/tests/Utils/UniqueIdentifierGeneratorTest.php b/tests/src/Utils/UniqueIdentifierGeneratorTest.php similarity index 95% rename from tests/Utils/UniqueIdentifierGeneratorTest.php rename to tests/src/Utils/UniqueIdentifierGeneratorTest.php index a6a87fa4..17c78b98 100644 --- a/tests/Utils/UniqueIdentifierGeneratorTest.php +++ b/tests/src/Utils/UniqueIdentifierGeneratorTest.php @@ -1,5 +1,7 @@