├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .php_cs.dist ├── CONFORMANCE_TEST.md ├── FAQ.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── bin └── install.php ├── composer.json ├── config └── module_oidc.php.dist ├── conformance-tests ├── basic-skips.json ├── basic-warnings.json ├── conformance-back-channel-logout-ci.json ├── conformance-basic-ci.json ├── conformance-basic-deployed.json ├── conformance-basic-local.json ├── conformance-implicit-ci.json ├── conformance-rp-initiated-logout-ci.json ├── implicit-skips.json ├── implicit-warnings.json └── sample-warnings.json ├── docker ├── Dockerfile ├── apache-override.cf ├── conformance.sql ├── docker-compose.yml ├── nginx-certs │ ├── README.md │ ├── default.crt │ ├── default.key │ └── expiration └── ssp │ ├── authsources.php │ ├── config-override.php │ ├── module_oidc.php │ ├── oidc_module.crt │ └── oidc_module.key ├── docs └── oidc.png ├── hooks ├── hook_adminmenu.php ├── hook_cron.php └── hook_federationpage.php ├── locales ├── en │ └── LC_MESSAGES │ │ └── oidc.po ├── es │ └── LC_MESSAGES │ │ └── oidc.po ├── fr │ └── LC_MESSAGES │ │ └── oidc.po ├── hr │ └── LC_MESSAGES │ │ └── oidc.po ├── it │ └── LC_MESSAGES │ │ └── oidc.po └── nl │ └── LC_MESSAGES │ └── oidc.po ├── phpcs.xml ├── phpunit.integration.xml ├── phpunit.xml ├── psalm.xml ├── public ├── assets │ ├── css │ │ └── src │ │ │ └── default.css │ └── js │ │ └── src │ │ ├── client-form.js │ │ └── default.js ├── authorize.php ├── jwks.php ├── logout.php ├── openid-configuration.php ├── token.php └── userinfo.php ├── rector.php ├── routing ├── routes │ └── routes.php └── services │ └── services.yml ├── src ├── Admin │ ├── Authorization.php │ ├── Menu.php │ └── Menu │ │ └── Item.php ├── Bridges │ ├── PsrHttpBridge.php │ ├── SspBridge.php │ └── SspBridge │ │ ├── Auth.php │ │ ├── Auth │ │ └── Source.php │ │ ├── Module.php │ │ ├── Module │ │ └── Admin.php │ │ └── Utils.php ├── Codebooks │ ├── DateFormatsEnum.php │ ├── LimitsEnum.php │ ├── ParametersEnum.php │ ├── RegistrationTypeEnum.php │ └── RoutesEnum.php ├── Controllers │ ├── AccessTokenController.php │ ├── Admin │ │ ├── ClientController.php │ │ ├── ConfigController.php │ │ └── TestController.php │ ├── AuthorizationController.php │ ├── ConfigurationDiscoveryController.php │ ├── EndSessionController.php │ ├── Federation │ │ ├── EntityStatementController.php │ │ └── SubordinateListingsController.php │ ├── JwksController.php │ ├── Traits │ │ └── RequestTrait.php │ └── UserInfoController.php ├── Entities │ ├── AccessTokenEntity.php │ ├── AuthCodeEntity.php │ ├── ClaimSetEntity.php │ ├── ClientEntity.php │ ├── Interfaces │ │ ├── AccessTokenEntityInterface.php │ │ ├── AuthCodeEntityInterface.php │ │ ├── ClaimSetEntityInterface.php │ │ ├── ClaimSetInterface.php │ │ ├── ClientEntityInterface.php │ │ ├── EntityStringRepresentationInterface.php │ │ ├── MementoInterface.php │ │ ├── RefreshTokenEntityInterface.php │ │ ├── ScopeInterface.php │ │ ├── TokenAssociatableWithAuthCodeInterface.php │ │ └── TokenRevokableInterface.php │ ├── RefreshTokenEntity.php │ ├── ScopeEntity.php │ ├── Traits │ │ ├── AssociateWithAuthCodeTrait.php │ │ ├── OidcAuthCodeTrait.php │ │ └── RevokeTokenTrait.php │ └── UserEntity.php ├── Exceptions │ ├── AuthorizationException.php │ └── OidcException.php ├── Factories │ ├── AuthSimpleFactory.php │ ├── AuthorizationServerFactory.php │ ├── CacheFactory.php │ ├── ClaimTranslatorExtractorFactory.php │ ├── CoreFactory.php │ ├── CryptKeyFactory.php │ ├── Entities │ │ ├── AccessTokenEntityFactory.php │ │ ├── AuthCodeEntityFactory.php │ │ ├── ClaimSetEntityFactory.php │ │ ├── ClientEntityFactory.php │ │ ├── RefreshTokenEntityFactory.php │ │ ├── ScopeEntityFactory.php │ │ └── UserEntityFactory.php │ ├── FederationFactory.php │ ├── FormFactory.php │ ├── Grant │ │ ├── AuthCodeGrantFactory.php │ │ ├── ImplicitGrantFactory.php │ │ └── RefreshTokenGrantFactory.php │ ├── IdTokenResponseFactory.php │ ├── JwksFactory.php │ ├── ProcessingChainFactory.php │ ├── RequestRulesManagerFactory.php │ ├── ResourceServerFactory.php │ └── TemplateFactory.php ├── Forms │ ├── ClientForm.php │ └── Controls │ │ └── CsrfProtection.php ├── Helpers.php ├── Helpers │ ├── Arr.php │ ├── Client.php │ ├── DateTime.php │ ├── Http.php │ ├── Random.php │ ├── Scope.php │ └── Str.php ├── ModuleConfig.php ├── Repositories │ ├── AbstractDatabaseRepository.php │ ├── AccessTokenRepository.php │ ├── AllowedOriginRepository.php │ ├── AuthCodeRepository.php │ ├── ClientRepository.php │ ├── CodeChallengeVerifiersRepository.php │ ├── Interfaces │ │ ├── AccessTokenRepositoryInterface.php │ │ ├── AuthCodeRepositoryInterface.php │ │ ├── IdentityProviderInterface.php │ │ └── RefreshTokenRepositoryInterface.php │ ├── RefreshTokenRepository.php │ ├── ScopeRepository.php │ └── UserRepository.php ├── Server │ ├── Associations │ │ ├── Interfaces │ │ │ └── RelyingPartyAssociationInterface.php │ │ └── RelyingPartyAssociation.php │ ├── AuthorizationServer.php │ ├── Exceptions │ │ └── OidcServerException.php │ ├── Grants │ │ ├── AuthCodeGrant.php │ │ ├── ImplicitGrant.php │ │ ├── Interfaces │ │ │ ├── AuthorizationValidatableWithRequestRules.php │ │ │ ├── OidcCapableGrantTypeInterface.php │ │ │ └── PkceEnabledGrantTypeInterface.php │ │ ├── RefreshTokenGrant.php │ │ └── Traits │ │ │ └── IssueAccessTokenTrait.php │ ├── LogoutHandlers │ │ └── BackChannelLogoutHandler.php │ ├── RequestRules │ │ ├── Interfaces │ │ │ ├── RequestRuleInterface.php │ │ │ ├── ResultBagInterface.php │ │ │ └── ResultInterface.php │ │ ├── RequestRulesManager.php │ │ ├── Result.php │ │ ├── ResultBag.php │ │ └── Rules │ │ │ ├── AbstractRule.php │ │ │ ├── AcrValuesRule.php │ │ │ ├── AddClaimsToIdTokenRule.php │ │ │ ├── ClientAuthenticationRule.php │ │ │ ├── ClientIdRule.php │ │ │ ├── CodeChallengeMethodRule.php │ │ │ ├── CodeChallengeRule.php │ │ │ ├── CodeVerifierRule.php │ │ │ ├── IdTokenHintRule.php │ │ │ ├── MaxAgeRule.php │ │ │ ├── PostLogoutRedirectUriRule.php │ │ │ ├── PromptRule.php │ │ │ ├── RedirectUriRule.php │ │ │ ├── RequestObjectRule.php │ │ │ ├── RequestedClaimsRule.php │ │ │ ├── RequiredNonceRule.php │ │ │ ├── RequiredOpenIdScopeRule.php │ │ │ ├── ResponseTypeRule.php │ │ │ ├── ScopeOfflineAccessRule.php │ │ │ ├── ScopeRule.php │ │ │ ├── StateRule.php │ │ │ └── UiLocalesRule.php │ ├── RequestTypes │ │ ├── AuthorizationRequest.php │ │ └── LogoutRequest.php │ ├── ResponseTypes │ │ ├── IdTokenResponse.php │ │ └── Interfaces │ │ │ ├── AcrResponseTypeInterface.php │ │ │ ├── AuthTimeResponseTypeInterface.php │ │ │ ├── NonceResponseTypeInterface.php │ │ │ └── SessionIdResponseTypeInterface.php │ ├── TokenIssuers │ │ ├── AbstractTokenIssuer.php │ │ └── RefreshTokenIssuer.php │ └── Validators │ │ └── BearerTokenValidator.php ├── Services │ ├── AuthContextService.php │ ├── AuthenticationService.php │ ├── Container.php │ ├── DatabaseMigration.php │ ├── ErrorResponder.php │ ├── IdTokenBuilder.php │ ├── JsonWebKeySetService.php │ ├── JsonWebTokenBuilderService.php │ ├── LoggerService.php │ ├── LogoutTokenBuilder.php │ ├── OpMetadataService.php │ ├── RoutingService.php │ ├── SessionMessagesService.php │ ├── SessionService.php │ └── StateService.php ├── Stores │ └── Session │ │ ├── LogoutTicketStoreBuilder.php │ │ ├── LogoutTicketStoreDb.php │ │ └── LogoutTicketStoreInterface.php └── Utils │ ├── ClaimTranslatorExtractor.php │ ├── ClassInstanceBuilder.php │ ├── Debug │ └── ArrayLogger.php │ ├── FederationCache.php │ ├── FederationParticipationValidator.php │ ├── FingerprintGenerator.php │ ├── JwksResolver.php │ ├── ProtocolCache.php │ ├── RequestParamsResolver.php │ └── Routes.php ├── templates ├── base.twig ├── clients.twig ├── clients │ ├── add.twig │ ├── edit.twig │ ├── includes │ │ └── form.twig │ └── show.twig ├── config │ ├── federation.twig │ ├── migrations.twig │ └── protocol.twig ├── includes │ └── menu.twig ├── logout.twig └── tests │ ├── trust-chain-resolution.twig │ └── trust-mark-validation.twig └── tests ├── bootstrap.php ├── config ├── authsources.php ├── config.php └── module_oidc.php ├── integration └── src │ └── Repositories │ └── AccessTokenRepositoryTest.php └── unit └── src ├── Admin ├── AuthorizationTest.php ├── Menu │ └── ItemTest.php └── MenuTest.php ├── Bridges ├── PsrHttpBridgeTest.php ├── SspBridge │ ├── Auth │ │ └── SourceTest.php │ ├── AuthTest.php │ ├── Module │ │ └── AdminTest.php │ ├── ModuleTest.php │ └── UtilsTest.php └── SspBridgeTest.php ├── Codebooks └── RegistrationTypeEnumTest.php ├── Controllers ├── AccessTokenControllerTest.php ├── Admin │ ├── ClientControllerTest.php │ └── ConfigControllerTest.php ├── AuthorizationControllerTest.php ├── ConfigurationDiscoveryControllerTest.php ├── EndSessionControllerTest.php ├── Federation │ ├── EntityStatementControllerTest.php │ └── SubordinateListingsControllerTest.php ├── JwksControllerTest.php ├── Traits │ └── RequestTraitTest.php └── UserInfoControllerTest.php ├── Entities ├── AccessTokenEntityTest.php ├── AuthCodeEntityTest.php ├── ClaimSetEntityTest.php ├── ClientEntityTest.php ├── RefreshTokenEntityTest.php ├── ScopeEntityTest.php └── UserEntityTest.php ├── Factories ├── AuthSimpleFactoryTest.php ├── ClaimTranslatorExtractorFactoryTest.php ├── FormFactoryTest.php ├── ProcessingChainFactoryTest.php └── TemplateFactoryTest.php ├── Forms └── ClientFormTest.php ├── Helpers ├── ArrTest.php ├── ClientTest.php ├── DateTimeTest.php ├── HttpTest.php ├── RandomTest.php ├── ScopeTest.php └── StrTest.php ├── HelpersTest.php ├── ModuleConfigTest.php ├── Repositories ├── AbstractDatabaseRepositoryTest.php ├── AccessTokenRepositoryTest.php ├── AllowedOriginRepositoryTest.php ├── AuthCodeRepositoryTest.php ├── ClientRepositoryTest.php ├── CodeChallengeVerifiersRepositoryTest.php ├── RefreshTokenRepositoryTest.php ├── ScopeRepositoryTest.php └── UserRepositoryTest.php ├── Server ├── Associations │ └── RelyingPartyAssociationTest.php ├── AuthorizationServerTest.php ├── Grants │ ├── AuthCodeGrantTest.php │ └── ImplicitGrantTest.php ├── LogoutHandlers │ └── BackChannelLogoutHandlerTest.php ├── RequestRules │ ├── RequestRulesManagerTest.php │ ├── ResultBagTest.php │ ├── ResultTest.php │ └── Rules │ │ ├── AcrValuesRuleTest.php │ │ ├── AddClaimsToIdTokenRuleTest.php │ │ ├── ClientIdRuleTest.php │ │ ├── CodeChallengeMethodRuleTest.php │ │ ├── CodeChallengeRuleTest.php │ │ ├── IdTokenHintRuleTest.php │ │ ├── PostLogoutRedirectUriRuleTest.php │ │ ├── RedirectUriRuleTest.php │ │ ├── RequestObjectRuleTest.php │ │ ├── RequestedClaimsRuleTest.php │ │ ├── RequiredNonceRuleTest.php │ │ ├── RequiredOpenIdScopeRuleTest.php │ │ ├── ResponseTypeRuleTest.php │ │ ├── ScopeOfflineAccessRuleTest.php │ │ ├── ScopeRuleTest.php │ │ ├── StateRuleTest.php │ │ └── UiLocalesRuleTest.php ├── RequestTypes │ ├── AuthorizationRequestTest.php │ └── LogoutRequestTest.php ├── ResponseTypes │ └── IdTokenResponseTest.php └── Validators │ └── BearerTokenValidatorTest.php ├── Services ├── AuthContextServiceTest.php ├── AuthenticationServiceTest.php ├── IdTokenBuilderTest.php ├── JsonWebKeySetServiceTest.php ├── JsonWebTokenBuilderServiceTest.php ├── LogoutTokenBuilderTest.php ├── OpMetadataServiceTest.php ├── SessionMessagesServiceTest.php ├── SessionServiceTest.php └── StateServiceTest.php ├── Stores └── Session │ ├── LogoutTicketStoreBuilderTest.php │ └── LogoutTicketStoreDbTest.php └── Utils ├── ClaimTranslatorExtractorTest.php ├── Debug └── ArrayLoggerTest.php ├── FederationParticipationValidatorTest.php └── RequestParamsResolverTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /vendor/ 3 | /.php_cs.cache 4 | /composer.lock 5 | /phpunit.yml 6 | .phpunit.result.cache 7 | /.idea/ 8 | *~ -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | in(__DIR__) 16 | ; 17 | return PhpCsFixer\Config::create() 18 | ->setRiskyAllowed(true) 19 | ->setRules([ 20 | '@Symfony' => true, 21 | '@Symfony:risky' => true, 22 | 'array_syntax' => ['syntax' => 'short'], 23 | 'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'], 24 | 'linebreak_after_opening_tag' => true, 25 | 'mb_str_functions' => true, 26 | 'no_php4_constructor' => true, 27 | 'no_unreachable_default_argument_value' => true, 28 | 'no_useless_else' => true, 29 | 'no_useless_return' => true, 30 | 'ordered_imports' => true, 31 | 'phpdoc_order' => true, 32 | 'phpdoc_to_comment' => false, 33 | 'semicolon_after_instruction' => true, 34 | 'strict_comparison' => true, 35 | 'strict_param' => true, 36 | ]) 37 | ->setFinder($finder) 38 | ; 39 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Set JSON type for claims 2 | 3 | You can set the type of claim by prefixing the name with `int:`, `bool:` or `string:`. If no prefix is set then `string` 4 | is assumed. In the rare event that your custom claim name starts with a prefix (example: `int:mycustomclaim`) you can 5 | add one of the type prefixes (example: `string:int:mycustomclaim`) to force the module to release a claim with the 6 | original prefix in it (example: claim `int:mycustomclaim` of type `string`) 7 | 8 | # Release photo 9 | 10 | The OIDC `picture` claim is an URL, while the `jpegPhoto` LDAP attribute is often a b64 string. To use `jpegPhoto` you 11 | can try using an authproc filter to turn it into a data url by adding `data:image/jpeg;base64,` prefix. The support 12 | for data URLs amongst OIDC client is unknown. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2018 by the Spanish Research and Academic Network. 5 | 6 | This code was developed by Universidad de Córdoba (UCO https://www.uco.es) 7 | for the RedIRIS SIR service (SIR: http://www.rediris.es/sir) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. -------------------------------------------------------------------------------- /bin/install.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | isMigrated()) { 26 | echo 'Database is up to date, skipping.' . PHP_EOL; 27 | return 0; 28 | } 29 | 30 | echo 'Running database migrations.' . PHP_EOL; 31 | 32 | $databaseMigration->migrate(); 33 | 34 | echo 'Done running migrations.'; 35 | return 0; 36 | } catch (Throwable $exception) { 37 | echo 'There was an error while trying run database migrations: ' . $exception->getMessage(); 38 | return 1; 39 | } 40 | -------------------------------------------------------------------------------- /conformance-tests/basic-skips.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /conformance-tests/basic-warnings.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /conformance-tests/conformance-basic-deployed.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "discoveryUrl": "https://oidc-conformance.uat.cirrusidentity.com/.well-known/openid-configuration" 4 | }, 5 | "client": { 6 | "client_id": "_55a99a1d298da921cb27d700d4604352e51171ebc4", 7 | "client_secret": "_8967dd97d07cc59db7055e84ac00e79005157c1132" 8 | }, 9 | "client_secret_post": { 10 | "client_id": "_0afb7d18e54b2de8205a93e38ca119e62ee321d031", 11 | "client_secret": "_944e73bbeec7850d32b68f1b5c780562c955967e4e" 12 | }, 13 | "client2": { 14 | "client_id": "_34efb61060172a11d62101bc804db789f8f9100b0e", 15 | "client_secret": "_91a4607a1c10ba801268929b961b3f6c067ff82d21" 16 | }, 17 | "alias": "simplesamlphp-module-oidc", 18 | "description": "Testing of simplesamlphp-module-oidc running in publicly accessible location" 19 | } -------------------------------------------------------------------------------- /conformance-tests/conformance-basic-local.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "discoveryUrl": "https://op.local.stack-dev.cirrusidentity.com/.well-known/openid-configuration" 4 | }, 5 | "client": { 6 | "client_id": "_55a99a1d298da921cb27d700d4604352e51171ebc4", 7 | "client_secret": "_8967dd97d07cc59db7055e84ac00e79005157c1132" 8 | }, 9 | "client_secret_post": { 10 | "client_id": "_0afb7d18e54b2de8205a93e38ca119e62ee321d031", 11 | "client_secret": "_944e73bbeec7850d32b68f1b5c780562c955967e4e" 12 | }, 13 | "client2": { 14 | "client_id": "_34efb61060172a11d62101bc804db789f8f9100b0e", 15 | "client_secret": "_91a4607a1c10ba801268929b961b3f6c067ff82d21" 16 | }, 17 | "alias": "simplesamlphp-module-oidc", 18 | "description": "Testing of simplesamlphp-module-oidc running locally. Use with local conformance suite." 19 | } -------------------------------------------------------------------------------- /conformance-tests/implicit-skips.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /conformance-tests/implicit-warnings.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /conformance-tests/sample-warnings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "test-name": "oidcc-ensure-request-with-acr-values-succeeds", 4 | "variant": { 5 | "server_metadata": "discovery", 6 | "client_registration": "static_client" 7 | }, 8 | "configuration-filename": "*", 9 | "current-block": "Verify authorization endpoint response", 10 | "condition": "ValidateIdTokenACRClaimAgainstAcrValuesRequest", 11 | "expected-result": "warning", 12 | "comment": "Not implemented yet" 13 | } 14 | ] -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cirrusid/simplesamlphp:v2.3.7 2 | #FROM cicnavi/simplesamlphp:dev 3 | 4 | RUN apt-get update && apt-get install -y sqlite3 5 | # Prepopulate the DB with items needed for testing 6 | ADD docker/conformance.sql /tmp/ 7 | # Preload some clients 8 | RUN sqlite3 /var/simplesamlphp/data/mydb.sq3 < /tmp/conformance.sql \ 9 | && chown www-data /var/simplesamlphp/data/mydb.sq3 10 | 11 | # --build-arg OIDC_VERSION 12 | ARG OIDC_VERSION="@dev" 13 | 14 | ENV STAGINGCOMPOSERREPOS=oidc \ 15 | COMPOSER_REQUIRE="simplesamlphp/simplesamlphp-module-oidc:$OIDC_VERSION" 16 | ADD . /var/simplesamlphp/staging-modules/oidc 17 | 18 | RUN /opt/simplesaml/staging-install.sh 19 | RUN /opt/simplesaml/module-setup.sh 20 | ADD docker/ssp/module_oidc.php /var/simplesamlphp/config/module_oidc.php 21 | ADD docker/ssp/authsources.php /var/simplesamlphp/config/authsources.php 22 | ADD docker/ssp/config-override.php /var/simplesamlphp/config/config-override.php 23 | ADD docker/ssp/oidc_module.crt /var/simplesamlphp/cert/oidc_module.crt 24 | ADD docker/ssp/oidc_module.key /var/simplesamlphp/cert/oidc_module.key 25 | ADD docker/apache-override.cf /etc/apache2/sites-enabled/ssp-override.cf 26 | 27 | RUN chown www-data /var/simplesamlphp/cert/oidc* \ 28 | && chmod 660 /var/simplesamlphp/cert/oidc* 29 | -------------------------------------------------------------------------------- /docker/apache-override.cf: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule ^/.well-known/openid-configuration(.*) /${SSP_APACHE_ALIAS}module.php/oidc/.well-known/openid-configuration$1 [PT] 3 | RewriteRule ^/.well-known/openid-federation(.*) /${SSP_APACHE_ALIAS}module.php/oidc/.well-known/openid-federation$1 [PT] 4 | 5 | # Leave Authorization header with Bearer tokens available in requests. 6 | # Solution 1: 7 | RewriteEngine On 8 | RewriteCond %{HTTP:Authorization} .+ 9 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 10 | # Solution 2: 11 | #SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 12 | 13 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | http-proxy: 3 | image: nginxproxy/nginx-proxy:1.2.1 4 | ports: 5 | - "443:443" 6 | - "80:80" 7 | volumes: 8 | - /var/run/docker.sock:/tmp/docker.sock:ro 9 | - ./docker/nginx-certs/:/etc/nginx/certs 10 | links: 11 | - oidc-op 12 | # - oidc-rp 13 | oidc-op: 14 | hostname: op.local.stack-dev.cirrusidentity.com 15 | build: 16 | context: . 17 | dockerfile: docker/Dockerfile 18 | args: 19 | OIDC_VERSION: "${OIDC_VERSION}" 20 | environment: 21 | - STAGINGCOMPOSERREPOS=oidc 22 | - SSP_ADMIN_PASSWORD=secret1 23 | - VIRTUAL_HOST=op.local.stack-dev.cirrusidentity.com 24 | - VIRTUAL_PORT=443 25 | - VIRTUAL_PROTO=https 26 | - CERT_NAME=default 27 | volumes: 28 | - ./:/var/simplesamlphp/staging-modules/oidc:ro 29 | - ./docker/ssp/module_oidc.php:/var/simplesamlphp/config/module_oidc.php:ro 30 | - ./docker/ssp/authsources.php:/var/simplesamlphp/config/authsources.php:ro 31 | - ./docker/ssp/config-override.php:/var/simplesamlphp/config/config-override.php:ro 32 | - ./docker/apache-override.cf:/etc/apache2/sites-enabled/ssp-override.cf:ro 33 | # oidc-rp still need work 34 | # oidc-rp: 35 | # ports: 36 | # - "12080:80" 37 | # environment: 38 | # - OID_DISCOVERY=https://op.local.stack-dev.cirrusidentity.com/.well-known/openid-configuration 39 | # - OID_CLIENT_ID=replacewithclientid 40 | # - OID_CLIENT_SECRET=replacewithclientsecret 41 | # - PROXY_HOST=my-service 42 | # - PROXY_PORT=443 43 | # - PROXY_PROTOCOL=https 44 | # - VIRTUAL_HOST=rp.local.stack-dev.cirrusidentity.com 45 | # - VIRTUAL_PORT=80 46 | # - VIRTUAL_PROTO=http 47 | # - CERT_NAME=default 48 | # image: evry/oidc-proxy 49 | 50 | # Connect to the conformance-suite network to make it easier to communicate between components 51 | networks: 52 | default: 53 | name: conformance-suite_default 54 | external: true 55 | -------------------------------------------------------------------------------- /docker/nginx-certs/README.md: -------------------------------------------------------------------------------- 1 | Every 90 days these certificates expire. The upstream project/container will refresh its certs occasionally, and we 2 | can sync them here. 3 | 4 | ```bash 5 | docker pull cirrusid/simplesamlphp:latest 6 | docker run -v $PWD:/opt/tmp/certs cirrusid/simplesamlphp /bin/bash -c 'cp /etc/ssl/certs/${APACHE_CERT_NAME}.key /opt/tmp/certs/default.crt && cp /etc/ssl/private/${APACHE_CERT_NAME}.key /opt/tmp/certs/default.key && openssl x509 -noout -enddate -in /opt/tmp/certs/default.crt > /opt/tmp/certs/expiration' 7 | ``` 8 | 9 | The file `expiration` will get updated with the current expiration date of the certificates. 10 | -------------------------------------------------------------------------------- /docker/nginx-certs/default.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDVBiNCVGdAB4N8 3 | UYry0bEYdLfmHB8PjfnTQ+XtcRsjjfK8YlDcWYyCYVF+lK7KUjw0I+N+8Q3bx9TP 4 | F23YaUebMePyuE9tD7U0jA6bhZ6VXTN834cvJvaNIpIceOLZYNU+/i74wxfcmOHx 5 | VMvN4h3rHfz/C/3Hn6+ZM4F52bYclhfrKFHBSdzbkyPJyPaaVk2oQMUOXKttB0Oo 6 | bvrYVIjxN/W8+swz1I5ITIP8VgHpjUI97RKZ5QYunsG0uL1stXSfhwhOJH89xYwc 7 | fo11EVkNji6giB/9ok/MEKGqgdcyo19gL6dN7euhJZzMaOMfhW/cCQeb1cFcTGuM 8 | 121TUBqHAgMBAAECggEAB/UCvCeK88lUEAC7v/Y1N0Sk2eOTBXG4MzwGCqh+6wUS 9 | XBcQDisKJJSeBqxnGweXWBs/FC7M5bjBKjslzz+ffRyP9zELRnefvSa+JPEIy2t/ 10 | 0NpIompCK2NvMcESOCx1yrST7Jbc/VB4oBsawcYAeBfWq3A3Oo2scXyLCZIoS0j+ 11 | fuEM+s1gg5+vqbYb2+0KUcth2pPRSnHzJXtu6nN5YICgnQpHXfhCyWV2XS36lbeN 12 | m6+hRz4w+VJUAomfIo5ahe6LuYbZt77thOPgf1xWYU+GM+HQUytJPRFjfJPna39G 13 | 88SVxvqWAxlfb2tdVeIM6EiSpUr2ncgITylYFy7DgQKBgQD6PQhoK35Cw2SEwWNX 14 | BnoeZvC4YmGJEK8fkp3hri/A3NJ3y0GvLQV8e5LGt9O1xgFp6PdxwpbJhyMy97sN 15 | GxNQcxn8+Xa89JsQQGbjz7LHfUMKqVg+xq8E4a4rKauYOReVvr98grBxLdm0DZNO 16 | Vb6ntbbtsEkO3mBniUDrfES4swKBgQDZ7cD5E+ArjvVjfOrlo6lV3FMDrrv8DwnA 17 | ls1Kvs8MQ9+E4YZ01/+DZ3iCbeKH/NbKm4XXVoZEPA1nvulvdrTaGr7bowzKzHHh 18 | hpz0mfzsKZxXeogemiDYQ6BQO3Z0lHQbTJJOZSMFkXp+agt2ikJOmTLXNxgAN8nq 19 | KeYQAdS43QKBgB+SxdXG7xZjavJpKCyZz5y4ZlUNbLsLlN0J9cu825+c/R1KUw5U 20 | QuXy/ZD/LsI3qoP/dgEviTECUQmkQkCkEurKqxPFMhsjTdFeHt1NnoQXJPdaaJz7 21 | GqgmBYDCsDjzsysctzJxluug2mAielye6wBkKCGTZZRvsIA/zCYqNs2LAoGBANNx 22 | xnElIrTAoUClPDgRIkSHYBhLmmNGp/yvlII4PiW1WRLRyqZlyKlTZG6QdWHiJPky 23 | CptTfTSJW6xUZKPcdj7EAniSa9/8m2XpOTJukiMFgIa0AYxHmSScANi3yQf13e16 24 | zt23bVKCw2oSNAsQvKMMK3L7JpNXjdZgTrMrQ50VAoGBAIqSg3w2wnjsHrqryGQa 25 | jIUCm80EDx5t2tqGAn23RbR3ps+tSRB6KLjaZM+S90SzFRZjI3shA60tXTj2Mra/ 26 | xcJpc828KgGnyZIRB2gmO/YFURwLEx3dOwWlTS8wfRb3inCGydwQu2A+V59CLmck 27 | VTUEdI5qZrc1Aq/5OjkFI1GJ 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /docker/nginx-certs/expiration: -------------------------------------------------------------------------------- 1 | notAfter=Feb 28 22:58:22 2022 GMT 2 | -------------------------------------------------------------------------------- /docker/ssp/authsources.php: -------------------------------------------------------------------------------- 1 | array( 9 | 'core:AdminPassword', 10 | ), 11 | 12 | 'example-userpass' => [ 13 | 'exampleauth:UserPass', 14 | 'users' => [ 15 | 'student:studentpass' => [ 16 | 'uid' => ['student'], 17 | 'eduPersonAffiliation' => ['member', 'student'], 18 | 'eduPersonNickname' => 'Sir_Nickname', 19 | 'displayName' => 'Some User', 20 | 'givenName' => 'Firsty', 21 | 'middle_name' => 'Mid', 22 | 'sn' => 'Lasty', 23 | 'labeledURI' => 'https://example.com/student', 24 | 'jpegURL' => 'https://example.com/student.jpg', 25 | 'mail' => 'something@example.com', 26 | 'email_verified' => 'yes', 27 | 'zoneinfo' => 'Europe/Paris', 28 | 'updated_at' => '1621374126', 29 | 'preferredLanguage' => 'fr-CA', 30 | 'website' => 'https://example.com/student-blog', 31 | 'gender' => 'female', 32 | 'birthdate' => '1945-03-21', 33 | 'eduPersonUniqueId' => '13579', 34 | 'phone_number_verified' => 'yes', 35 | 'mobile' => '+1 (604) 555-1234;ext=5678', 36 | 'postalAddress' => ["Place Charles de Gaulle, Paris"], 37 | 'street_address' => ['Place Charles de Gaulle'], 38 | 'locality' => ['Paris'], 39 | 'region' => ['Île-de-France'], 40 | 'postal_code' => ['75008'], 41 | 'country' => ['France'], 42 | ], 43 | 'employee:employeepass' => [ 44 | 'uid' => ['employee'], 45 | 'eduPersonAffiliation' => ['member', 'employee'], 46 | 'eduPersonEntitlement' => ['urn:example:oidc:manage:client'] 47 | ], 48 | 'member:memberpass' => [ 49 | 'uid' => ['member'], 50 | 'eduPersonAffiliation' => ['member'], 51 | 'eduPersonEntitlement' => ['urn:example:oidc:manage:client'] 52 | ], 53 | 'minimal:minimalpass' => [ 54 | 'uid' => ['minimal'], 55 | ], 56 | ], 57 | ], 58 | 59 | 60 | ); 61 | -------------------------------------------------------------------------------- /docker/ssp/config-override.php: -------------------------------------------------------------------------------- 1 | 'testsalt', 11 | 'database.dsn' => getenv('DB.DSN') ?: 'sqlite:/var/simplesamlphp/data/mydb.sq3', 12 | 'database.username' => getenv('DB.USERNAME') ?: 'user', 13 | 'database.password' => getenv('DB.PASSWORD') ?: 'password', 14 | 'language.i18n.backend' => 'gettext/gettext', 15 | 'logging.level' => 7, 16 | 'usenewui' => false, 17 | ] + $config; -------------------------------------------------------------------------------- /docker/ssp/oidc_module.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq2EgIfAMG2XpfjRCAUHQ 3 | QqLAbrOL4s+JwL0X3jO2imVzbBp9MadLzGQhczyhKHM0B9YumHSwNjD5hrQQso02 4 | CaGientCzhm9PqerjffZ+0B4+FfGws0ozwyfA7hW1arhBx92D8r7Hw0HHu1QwSQ2 5 | N4zlmCea7shWTC4CoO8ECJcPDYe2/wABLCPZm0dcv/sPYln7HAiukI2fxwMpf3yQ 6 | XihcQQXdHHoOncUn7QlibUQj//Zxk0obEkJAUyqxFKa+3cuToFkfSH7bGs8YnI3q 7 | y/YzmsutI3keeEIQOTNAtLgauqZ4CW+Your+9vsUXaNjNshnObNHWLPc37DJaRyA 8 | pwIDAQAB 9 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /docker/ssp/oidc_module.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAq2EgIfAMG2XpfjRCAUHQQqLAbrOL4s+JwL0X3jO2imVzbBp9 3 | MadLzGQhczyhKHM0B9YumHSwNjD5hrQQso02CaGientCzhm9PqerjffZ+0B4+FfG 4 | ws0ozwyfA7hW1arhBx92D8r7Hw0HHu1QwSQ2N4zlmCea7shWTC4CoO8ECJcPDYe2 5 | /wABLCPZm0dcv/sPYln7HAiukI2fxwMpf3yQXihcQQXdHHoOncUn7QlibUQj//Zx 6 | k0obEkJAUyqxFKa+3cuToFkfSH7bGs8YnI3qy/YzmsutI3keeEIQOTNAtLgauqZ4 7 | CW+Your+9vsUXaNjNshnObNHWLPc37DJaRyApwIDAQABAoIBAE0+msARNTPILITQ 8 | wwtUAa13M+rxjFRvnLQ9xptFjbo1Xd/U1Kbjs9ttKlKJek4EFuiNVjUrKx1R17Yq 9 | RPhlg3y12MkB86t3mH+8DSwREbQYbC3rSlAVLpacJrQDi0gFHCYcvRcDM0rckWAU 10 | MPjM/I7vN7Dr8P49WABAILku4g+IYaEDUXMGjNnLwgbK6nXCyOn9pwKSqA0SYltI 11 | X63Ba1N13oCigR7UtHvHcRHEWzPkBJO5/pLnNfF+0Tn2z7WXVe5oxCar9nTrUfIP 12 | aspwSjGam7cFbyutnlRLX5/Rkk7r4PkXShP++08GxYTzV7nLhwpN33W0F6B8HLEH 13 | Nu56ihECgYEA20t5gbnRF6QFabJ277uTJ24jdpitB5UOIA8+K9/cPnBsSCfWjexY 14 | Nl0UYuJap3W80dL6lV8VuJZZ0ojAxE+Qwh04MZZXq/Egxn6l+6TNeUCbBHaaj1pl 15 | X0vdxIpC5BKmXhwLLtuqzVg7t3O1NK0c0tZ+kipUA7DenVT2ZHGCyA8CgYEAyBCH 16 | /GFbqsq3Oq8CURZ9osVq5eNXrXeiIp3OlbpywDYFNPLg91mck71gT6btZeuaAH7/ 17 | q7unxZ007ADAoKGPvRKtGa25RB5Vy+dC7F4EDdzz/UBh+fl2zg33W7toh9Kb2OGd 18 | HycEWgU4z+ZpC5fH0v892TlAqPQTFQx+F9rx5ekCgYEAsme7qWtHjUkWUkArfKuI 19 | cyqqVUCufB2qiTB9bupHXtDNdwJaDco6lbex7yShhd1GSRmwXTcnD63Z02sIEG1+ 20 | oj1tSwI5vxuDg5jjZk9UDpIdy0rGQVvUXuv0toGZG72Edcmw22VAlqByrLPIttsj 21 | OO/htv4SrZIF+c92SI8ES8cCgYEAmxktwzva69pCGE2K10A/YAv6ZoRL+aAwYvPC 22 | LxOfWGHIwZa1Tyz6lRKQcs+vZX80IcRTA1j0pN/OIlQnAaCepW6wIaMraKK30t7T 23 | ZBkyvWiZArGCA2AheXccV9I/JGTjC01FGNyPpBY+R/aRYzpk4K+dzCR1e0XU8VGB 24 | A49qTtkCgYEAhQqvYFBlPo5T/W6tWxLLcrvUXIUL2zt4qpKKvdKXTbYwWyUaVp6H 25 | Bu/fG1KQotNwpKDwgIb9Zv/obyW9wFhhNpobvvI273wyFZMps5bu50WIOggFErak 26 | GwPXiagzBlhklsfR+TpFYQ6MTo3ymhLy2+wVIOqV/tjUaRkT7kNk98U= 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /docs/oidc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplesamlphp/simplesamlphp-module-oidc/0080cf2ca10a6c00a80835a9e83638245ce9ea82/docs/oidc.png -------------------------------------------------------------------------------- /hooks/hook_adminmenu.php: -------------------------------------------------------------------------------- 1 | data[$menuKey]) || !is_array($template->data[$menuKey])) { 16 | return; 17 | } 18 | 19 | $moduleConfig = new ModuleConfig(); 20 | 21 | $oidcMenuEntry = [ 22 | ModuleConfig::MODULE_NAME => [ 23 | 'url' => $moduleConfig->getModuleUrl(RoutesEnum::AdminMigrations->value), 24 | 'name' => Translate::noop('OIDC'), 25 | ], 26 | ]; 27 | 28 | // Put OIDC entry before the 'Log out' entry, if it exists. 29 | $logoutEntryKey = 'logout'; 30 | $logoutEntryValue = null; 31 | if ( 32 | array_key_exists($logoutEntryKey, $template->data[$menuKey]) && 33 | is_array($template->data[$menuKey][$logoutEntryKey]) 34 | ) { 35 | $logoutEntryValue = $template->data[$menuKey][$logoutEntryKey]; 36 | unset($template->data[$menuKey][$logoutEntryKey]); 37 | } 38 | 39 | $template->data[$menuKey] += $oidcMenuEntry; 40 | 41 | if ($logoutEntryValue !== null) { 42 | $template->data[$menuKey][$logoutEntryKey] = $logoutEntryValue; 43 | } 44 | 45 | $template->getLocalization()->addModuleDomain(ModuleConfig::MODULE_NAME); 46 | } 47 | -------------------------------------------------------------------------------- /hooks/hook_federationpage.php: -------------------------------------------------------------------------------- 1 | urlAdminClients(); 34 | $text = Translate::noop('OIDC Client Registry'); 35 | 36 | if (! (new DatabaseMigration())->isMigrated()) { 37 | $href = $routes->urlAdminMigrations(); 38 | $text = Translate::noop('OIDC Installation'); 39 | } 40 | 41 | if (!is_array($template->data['links'])) { 42 | $template->data['links'] = []; 43 | } 44 | 45 | $template->data['links'][] = [ 46 | 'href' => $href, 47 | 'text' => $text, 48 | ]; 49 | } 50 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | By default it is less stringent about long lines than other coding standards 7 | 8 | 9 | 10 | 11 | 12 | config 13 | hooks 14 | src 15 | tests 16 | public 17 | routing/routes 18 | 19 | 20 | 21 | 22 | public/assets/* 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /phpunit.integration.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/integration 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ./src 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/unit 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ./src 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/assets/css/src/default.css: -------------------------------------------------------------------------------- 1 | 2 | h3 { 3 | margin-bottom: 0.5em; 4 | font-size: 1.2em; 5 | font-weight: 600; 6 | color: #1c1c1c; 7 | } 8 | 9 | h4 { 10 | margin: 0.4em 0; 11 | font-size: 1.0em; 12 | font-weight: 600; 13 | color: #1c1c1c; 14 | } 15 | 16 | /* Container to hold menu and content */ 17 | .oidc-container { 18 | display: flex; 19 | max-width: inherit; 20 | margin: 0 auto; 21 | } 22 | 23 | /* Style for the left menu */ 24 | .menu { 25 | min-width: 200px; 26 | width: auto; 27 | } 28 | 29 | /* Style for the menu items */ 30 | .menu ul { 31 | list-style-type: none; 32 | padding: 0; 33 | } 34 | 35 | .menu ul li { 36 | padding: 0.25rem; 37 | } 38 | 39 | .menu ul li a { 40 | text-decoration: none; 41 | color: #333; 42 | display: block; 43 | padding: 0.5rem; 44 | } 45 | 46 | .menu ul li a:hover { 47 | background-color: #ddd; 48 | padding: 0.5rem; 49 | } 50 | 51 | .menu ul li a.active { 52 | background-color: #eeeeee; 53 | padding: 0.5rem; 54 | } 55 | 56 | /* Style for the content area */ 57 | .content { 58 | flex-grow: 1; 59 | padding-left: 20px; 60 | max-width: inherit; 61 | background-color: #fff; 62 | } 63 | 64 | ul.disc { 65 | list-style: disc outside none; 66 | } 67 | 68 | em { 69 | font-style: italic; 70 | } 71 | 72 | /* Text colors */ 73 | .black-text { color: black; } 74 | .red-text { color: red; } 75 | .lightcoral-text { color: lightcoral; } 76 | .green-text { color: green; } 77 | .yellow-text { color: yellow; } 78 | .blue-text { color: blue; } 79 | .magenta-text { color: magenta; } 80 | .cyan-text { color: cyan; } 81 | .lightcyan-text { color: lightcyan; } 82 | .white-text { color: white; } 83 | 84 | /* Button sizes */ 85 | .button-small { 86 | font-size: 75%; 87 | } 88 | 89 | /* Client Table */ 90 | table.client-table { 91 | width: 100%; 92 | } 93 | 94 | .client-col.col-info { 95 | width: 79%; 96 | } 97 | 98 | .client-col.col-actions { 99 | width: 21%; 100 | } 101 | 102 | .client-col.col-property { 103 | width: 25%; 104 | font-weight: bolder; 105 | } 106 | 107 | .confirm-action {} 108 | 109 | form.pure-form-stacked .full-width { 110 | width: 100%; 111 | } 112 | -------------------------------------------------------------------------------- /public/assets/js/src/client-form.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // Handle enabling and disabling input for 'allowed origins', based on client type radio input. 5 | function toggleAllowedOrigins() { 6 | if (radioOptionPublic.checked) { 7 | inputAllowedOrigin.disabled = false; // Enable the input field 8 | } else if (radioOptionConfidential.checked) { 9 | inputAllowedOrigin.disabled = true; // Disable the input field 10 | } 11 | } 12 | 13 | // Get references to the radio buttons and the input field 14 | const radioOptionPublic = document.getElementById("radio-option-public"); 15 | const radioOptionConfidential = document.getElementById("radio-option-confidential"); 16 | const inputAllowedOrigin = document.getElementById("frm-allowed_origin"); 17 | 18 | radioOptionPublic.addEventListener("change", toggleAllowedOrigins); 19 | radioOptionConfidential.addEventListener("change", toggleAllowedOrigins); 20 | 21 | toggleAllowedOrigins(); 22 | })(); 23 | -------------------------------------------------------------------------------- /public/assets/js/src/default.js: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | 4 | // Attach `confirm-action` click event to all elements with the `confirm-action` class. 5 | document.querySelectorAll('.confirm-action').forEach(button => { 6 | button.addEventListener('click', function (event) { 7 | // Get custom confirmation text 8 | const confirmText = this.getAttribute('data-confirm-text') ?? 'Are you sure?'; 9 | // Optional: Retrieve additional data 10 | const itemId = this.getAttribute('data-confirm-id') ?? 'N/A'; 11 | 12 | if (!confirm(confirmText)) { 13 | // Prevent the default action if the user cancels 14 | event.preventDefault(); 15 | } else { 16 | // Optional: Handle confirmed action 17 | console.log( 18 | `Confirmed action "${confirmText}" for item with ID "${itemId}"`); 19 | } 20 | }); 21 | }); 22 | })(); 23 | -------------------------------------------------------------------------------- /public/authorize.php: -------------------------------------------------------------------------------- 1 | importNames(); 12 | $rectorConfig->disableParallel(); 13 | 14 | $rectorConfig->bootstrapFiles([ 15 | //__DIR__ . '/vendor/autoload.php', 16 | ]); 17 | 18 | $rectorConfig->paths([ 19 | // TODO v7 mivanci also go trough commented out paths... 20 | //__DIR__ . '/docker', 21 | //__DIR__ . '/hooks', 22 | //__DIR__ . '/public', 23 | __DIR__ . '/src', 24 | __DIR__ . '/tests', 25 | ]); 26 | 27 | // register a single rule 28 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 29 | $rectorConfig->rule(DeclareStrictTypesRector::class); 30 | 31 | // define sets of rules 32 | $rectorConfig->sets([ 33 | LevelSetList::UP_TO_PHP_81, 34 | ]); 35 | }; 36 | -------------------------------------------------------------------------------- /src/Admin/Authorization.php: -------------------------------------------------------------------------------- 1 | sspBridge->utils()->auth()->isAdmin(); 24 | } 25 | 26 | /** 27 | * @throws \SimpleSAML\Module\oidc\Exceptions\AuthorizationException 28 | */ 29 | public function requireAdmin(bool $forceAdminAuthentication = false): void 30 | { 31 | if ($forceAdminAuthentication) { 32 | try { 33 | $this->sspBridge->utils()->auth()->requireAdmin(); 34 | } catch (Exception $exception) { 35 | throw new AuthorizationException( 36 | Translate::noop('Unable to initiate SimpleSAMLphp admin authentication.'), 37 | $exception->getCode(), 38 | $exception, 39 | ); 40 | } 41 | } 42 | 43 | if (! $this->isAdmin()) { 44 | throw new AuthorizationException(Translate::noop('SimpleSAMLphp admin access required.')); 45 | } 46 | } 47 | 48 | /** 49 | * @throws \SimpleSAML\Module\oidc\Exceptions\AuthorizationException 50 | */ 51 | public function requireAdminOrUserWithPermission(string $permission): void 52 | { 53 | if ($this->isAdmin()) { 54 | return; 55 | } 56 | 57 | try { 58 | $this->authContextService->requirePermission($permission); 59 | } catch (\Exception) { 60 | // TODO mivanci v7 log this exception 61 | } 62 | 63 | // If we get here, the user does not have the required permission, or permissions are not enabled. 64 | // Fallback to admin authentication. 65 | $this->requireAdmin(true); 66 | } 67 | 68 | public function getUserId(): string 69 | { 70 | return $this->authContextService->getAuthUserId(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Admin/Menu.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected array $items = []; 15 | 16 | protected ?string $activeHrefPath = null; 17 | 18 | public function __construct(Item ...$items) 19 | { 20 | array_push($this->items, ...$items); 21 | } 22 | 23 | public function addItem(Item $menuItem, ?int $offset = null): void 24 | { 25 | $offset ??= count($this->items); 26 | 27 | array_splice($this->items, $offset, 0, [$menuItem]); 28 | } 29 | 30 | public function getItems(): array 31 | { 32 | return $this->items; 33 | } 34 | 35 | public function setActiveHrefPath(?string $value): void 36 | { 37 | $this->activeHrefPath = $value; 38 | } 39 | 40 | public function getActiveHrefPath(): ?string 41 | { 42 | return $this->activeHrefPath; 43 | } 44 | 45 | /** 46 | * Item factory method for easy injection in tests. 47 | */ 48 | public function buildItem(string $hrefPath, string $label, ?string $iconAssetPath = null): Item 49 | { 50 | return new Item($hrefPath, $label, $iconAssetPath); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Admin/Menu/Item.php: -------------------------------------------------------------------------------- 1 | hrefPath; 19 | } 20 | 21 | public function getLabel(): string 22 | { 23 | return $this->label; 24 | } 25 | 26 | public function getIconAssetPath(): ?string 27 | { 28 | return $this->iconAssetPath; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Bridges/PsrHttpBridge.php: -------------------------------------------------------------------------------- 1 | psrHttpFactory = new PsrHttpFactory( 26 | $this->serverRequestFactory, 27 | $this->streamFactory, 28 | $this->uploadedFileFactory, 29 | $this->responseFactory, 30 | ); 31 | } 32 | 33 | public function getHttpFoundationFactory(): HttpFoundationFactory 34 | { 35 | return $this->httpFoundationFactory; 36 | } 37 | 38 | public function getServerRequestFactory(): ServerRequestFactoryInterface 39 | { 40 | return $this->serverRequestFactory; 41 | } 42 | 43 | public function getResponseFactory(): ResponseFactoryInterface 44 | { 45 | return $this->responseFactory; 46 | } 47 | 48 | public function getStreamFactory(): StreamFactoryInterface 49 | { 50 | return $this->streamFactory; 51 | } 52 | 53 | public function getUploadedFileFactory(): UploadedFileFactoryInterface 54 | { 55 | return $this->uploadedFileFactory; 56 | } 57 | 58 | public function getPsrHttpFactory(): PsrHttpFactory 59 | { 60 | return $this->psrHttpFactory; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Bridges/SspBridge.php: -------------------------------------------------------------------------------- 1 | Translate::noop('Manual'), 18 | self::FederatedAutomatic => Translate::noop('Federated Automatic'), 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Codebooks/RoutesEnum.php: -------------------------------------------------------------------------------- 1 | opMetadataService->getMetadata()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Controllers/JwksController.php: -------------------------------------------------------------------------------- 1 | array_values($this->jsonWebKeySetService->protocolKeys()), 36 | ]); 37 | } 38 | 39 | public function jwks(): Response 40 | { 41 | return $this->psrHttpBridge->getHttpFoundationFactory()->createResponse($this->__invoke()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Controllers/Traits/RequestTrait.php: -------------------------------------------------------------------------------- 1 | getHeaderLine('Origin'); 34 | 35 | if (empty($origin)) { 36 | throw OidcServerException::requestNotSupported('CORS error: no Origin header present'); 37 | } 38 | 39 | if (! $this->allowedOriginRepository->has($origin)) { 40 | throw OidcServerException::accessDenied(sprintf('CORS error: origin %s is not allowed', $origin)); 41 | } 42 | 43 | return $this->psrHttpBridge->getResponseFactory()->createResponse(204) 44 | ->withBody($this->psrHttpBridge->getStreamFactory()->createStream('php://memory')) 45 | ->withHeader('Access-Control-Allow-Origin', $origin) 46 | ->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 47 | ->withHeader('Access-Control-Allow-Headers', 'Authorization, X-Requested-With') 48 | ->withHeader('Access-Control-Allow-Credentials', 'true') 49 | ; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Entities/ClaimSetEntity.php: -------------------------------------------------------------------------------- 1 | 13 | * @copyright (c) 2018 Steve Rhoades 14 | * @license http://opensource.org/licenses/MIT MIT 15 | */ 16 | class ClaimSetEntity implements ClaimSetEntityInterface 17 | { 18 | public function __construct(protected string $scope, protected array $claims) 19 | { 20 | } 21 | 22 | public function getScope(): string 23 | { 24 | return $this->scope; 25 | } 26 | 27 | public function getClaims(): array 28 | { 29 | return $this->claims; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Entities/Interfaces/AccessTokenEntityInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright (c) 2018 Steve Rhoades 12 | * @license http://opensource.org/licenses/MIT MIT 13 | */ 14 | interface ClaimSetEntityInterface extends ClaimSetInterface, ScopeInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Entities/Interfaces/ClaimSetInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright (c) 2018 Steve Rhoades 12 | * @license http://opensource.org/licenses/MIT MIT 13 | */ 14 | interface ClaimSetInterface 15 | { 16 | public function getClaims(): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Entities/Interfaces/ClientEntityInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright (c) 2018 Steve Rhoades 12 | * @license http://opensource.org/licenses/MIT MIT 13 | */ 14 | interface ScopeInterface 15 | { 16 | public function getScope(): string; 17 | } 18 | -------------------------------------------------------------------------------- /src/Entities/Interfaces/TokenAssociatableWithAuthCodeInterface.php: -------------------------------------------------------------------------------- 1 | setIdentifier($id); 42 | $this->setExpiryDateTime($expiryDateTime); 43 | $this->setAccessToken($accessTokenEntity); 44 | $this->setAuthCodeId($authCodeId); 45 | $this->isRevoked = $isRevoked; 46 | } 47 | 48 | public function getState(): array 49 | { 50 | return [ 51 | 'id' => $this->getIdentifier(), 52 | 'expires_at' => $this->getExpiryDateTime()->format('Y-m-d H:i:s'), 53 | 'access_token_id' => $this->getAccessToken()->getIdentifier(), 54 | 'is_revoked' => $this->isRevoked(), 55 | 'auth_code_id' => $this->getAuthCodeId(), 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Entities/ScopeEntity.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 39 | } 40 | 41 | public function getIcon(): ?string 42 | { 43 | return $this->icon; 44 | } 45 | 46 | public function getDescription(): ?string 47 | { 48 | return $this->description; 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function getClaims(): array 55 | { 56 | return $this->claims; 57 | } 58 | 59 | public function jsonSerialize(): string 60 | { 61 | return (string) $this->getIdentifier(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Entities/Traits/AssociateWithAuthCodeTrait.php: -------------------------------------------------------------------------------- 1 | authCodeId = $authCodeId; 14 | } 15 | 16 | public function getAuthCodeId(): ?string 17 | { 18 | return $this->authCodeId; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Entities/Traits/OidcAuthCodeTrait.php: -------------------------------------------------------------------------------- 1 | nonce; 24 | } 25 | 26 | public function setNonce(string $nonce): void 27 | { 28 | $this->nonce = $nonce; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Entities/Traits/RevokeTokenTrait.php: -------------------------------------------------------------------------------- 1 | isRevoked; 25 | } 26 | 27 | /** 28 | * Revoke token. 29 | */ 30 | public function revoke(): void 31 | { 32 | $this->isRevoked = true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Entities/UserEntity.php: -------------------------------------------------------------------------------- 1 | $this->getIdentifier(), 44 | 'claims' => json_encode($this->getClaims(), JSON_INVALID_UTF8_SUBSTITUTE), 45 | 'updated_at' => $this->getUpdatedAt()->format('Y-m-d H:i:s'), 46 | 'created_at' => $this->getCreatedAt()->format('Y-m-d H:i:s'), 47 | ]; 48 | } 49 | 50 | public function getIdentifier(): string 51 | { 52 | return $this->identifier; 53 | } 54 | 55 | public function getClaims(): array 56 | { 57 | return $this->claims; 58 | } 59 | 60 | public function setClaims(array $claims): self 61 | { 62 | $this->claims = $claims; 63 | return $this; 64 | } 65 | 66 | public function getUpdatedAt(): DateTimeImmutable 67 | { 68 | return $this->updatedAt; 69 | } 70 | 71 | public function setUpdatedAt(DateTimeImmutable $updatedAt): self 72 | { 73 | $this->updatedAt = $updatedAt; 74 | return $this; 75 | } 76 | 77 | public function getCreatedAt(): DateTimeImmutable 78 | { 79 | return $this->createdAt; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Exceptions/AuthorizationException.php: -------------------------------------------------------------------------------- 1 | resolveAuthSourceId($clientEntity); 37 | 38 | return new Simple($authSourceId); 39 | } 40 | 41 | /** 42 | * @return Simple The default authsource 43 | * @throws \Exception 44 | */ 45 | public function getDefaultAuthSource(): Simple 46 | { 47 | return new Simple($this->moduleConfig->getDefaultAuthSourceId()); 48 | } 49 | 50 | /** 51 | * Get auth source defined on the client. If not set on the client, get the default auth source defined in config. 52 | * 53 | * @throws \Exception 54 | */ 55 | public function resolveAuthSourceId(ClientEntityInterface $client): string 56 | { 57 | return $client->getAuthSourceId() ?? $this->moduleConfig->getDefaultAuthSourceId(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Factories/CoreFactory.php: -------------------------------------------------------------------------------- 1 | moduleConfig->getFederationSigner()->algorithmId()), 31 | SignatureAlgorithmEnum::RS384, 32 | SignatureAlgorithmEnum::RS512, 33 | SignatureAlgorithmEnum::ES256, 34 | SignatureAlgorithmEnum::ES384, 35 | SignatureAlgorithmEnum::ES512, 36 | ), 37 | ); 38 | 39 | return new Core( 40 | supportedAlgorithms: $supportedAlgorithms, 41 | logger: $this->loggerService, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Factories/CryptKeyFactory.php: -------------------------------------------------------------------------------- 1 | moduleConfig->getProtocolPrivateKeyPath(), 24 | $this->moduleConfig->getProtocolPrivateKeyPassPhrase(), 25 | ); 26 | } 27 | 28 | /** 29 | * @throws \Exception 30 | */ 31 | public function buildPublicKey(): CryptKey 32 | { 33 | return new CryptKey($this->moduleConfig->getProtocolCertPath()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Factories/Entities/ClaimSetEntityFactory.php: -------------------------------------------------------------------------------- 1 | helpers->dateTime()->getUtc($state['expires_at']); 51 | $accessToken = $state['access_token']; 52 | $isRevoked = (bool) $state['is_revoked']; 53 | $authCodeId = empty($state['auth_code_id']) ? null : (string)$state['auth_code_id']; 54 | 55 | return $this->fromData( 56 | $id, 57 | $expiryDateTime, 58 | $accessToken, 59 | $authCodeId, 60 | $isRevoked, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Factories/Entities/ScopeEntityFactory.php: -------------------------------------------------------------------------------- 1 | helpers->dateTime()->getUtc(); 21 | 22 | return new UserEntity( 23 | $identifier, 24 | $createdAt, 25 | $updatedAt, 26 | $claims, 27 | ); 28 | } 29 | 30 | /** 31 | * @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException 32 | */ 33 | public function fromState(array $state): UserEntity 34 | { 35 | if ( 36 | !is_string($state['id']) || 37 | !is_string($state['claims']) || 38 | !is_string($state['updated_at']) || 39 | !is_string($state['created_at']) 40 | ) { 41 | throw OidcServerException::serverError('Invalid user entity data'); 42 | } 43 | 44 | $identifier = $state['id']; 45 | $claims = json_decode($state['claims'], true, 512, JSON_INVALID_UTF8_SUBSTITUTE); 46 | 47 | if (!is_array($claims)) { 48 | throw OidcServerException::serverError('Invalid user entity data'); 49 | } 50 | $updatedAt = $this->helpers->dateTime()->getUtc($state['updated_at']); 51 | $createdAt = $this->helpers->dateTime()->getUtc($state['created_at']); 52 | 53 | return new UserEntity( 54 | $identifier, 55 | $createdAt, 56 | $updatedAt, 57 | $claims, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Factories/FederationFactory.php: -------------------------------------------------------------------------------- 1 | moduleConfig->getFederationSigner()->algorithmId()), 33 | SignatureAlgorithmEnum::RS384, 34 | SignatureAlgorithmEnum::RS512, 35 | SignatureAlgorithmEnum::ES256, 36 | SignatureAlgorithmEnum::ES384, 37 | SignatureAlgorithmEnum::ES512, 38 | ), 39 | ); 40 | 41 | return new Federation( 42 | supportedAlgorithms: $supportedAlgorithms, 43 | maxCacheDuration: $this->moduleConfig->getFederationCacheMaxDurationForFetched(), 44 | cache: $this->federationCache?->cache, 45 | logger: $this->loggerService, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Factories/FormFactory.php: -------------------------------------------------------------------------------- 1 | moduleConfig, 50 | $this->csrfProtection, 51 | $this->sspBridge, 52 | $this->helpers, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Factories/Grant/ImplicitGrantFactory.php: -------------------------------------------------------------------------------- 1 | idTokenBuilder, 42 | $this->moduleConfig->getAccessTokenDuration(), 43 | $this->accessTokenRepository, 44 | $this->requestRulesManager, 45 | $this->requestParamsResolver, 46 | '#', 47 | $this->accessTokenEntityFactory, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Factories/Grant/RefreshTokenGrantFactory.php: -------------------------------------------------------------------------------- 1 | refreshTokenRepository, 38 | $this->accessTokenEntityFactory, 39 | $this->refreshTokenIssuer, 40 | ); 41 | 42 | $refreshTokenGrant->setRefreshTokenTTL($this->moduleConfig->getRefreshTokenDuration()); 43 | 44 | return $refreshTokenGrant; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Factories/IdTokenResponseFactory.php: -------------------------------------------------------------------------------- 1 | userRepository, 39 | $this->idTokenBuilder, 40 | $this->privateKey, 41 | ); 42 | $idTokenResponse->setEncryptionKey($this->moduleConfig->getEncryptionKey()); 43 | 44 | return $idTokenResponse; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Factories/JwksFactory.php: -------------------------------------------------------------------------------- 1 | moduleConfig->getFederationSigner()->algorithmId()), 33 | ), 34 | ); 35 | 36 | return new Jwks( 37 | supportedAlgorithms: $supportedAlgorithms, 38 | maxCacheDuration: $this->moduleConfig->getFederationCacheMaxDurationForFetched(), 39 | cache: $this->federationCache?->cache, 40 | logger: $this->loggerService, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Factories/ProcessingChainFactory.php: -------------------------------------------------------------------------------- 1 | $state['Source']['entityid'] ?? '', 32 | // ProcessChain needs to know the list of authproc filters we defined in module_oidc configuration 33 | 'authproc' => $this->moduleConfig->getAuthProcFilters(), 34 | ]; 35 | $spMetadata = [ 36 | 'entityid' => $state['Destination']['entityid'] ?? '', 37 | ]; 38 | 39 | return new ProcessingChain( 40 | $idpMetadata, 41 | $spMetadata, 42 | 'oidc', 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Factories/ResourceServerFactory.php: -------------------------------------------------------------------------------- 1 | accessTokenRepository, 37 | $this->publicKey, 38 | $this->authorizationValidator, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Forms/Controls/CsrfProtection.php: -------------------------------------------------------------------------------- 1 | setOmitted() 51 | ->setRequired() 52 | ->addRule(self::PROTECTION, $errorMessage); 53 | } 54 | 55 | /** 56 | * @throws \Exception 57 | */ 58 | public function getToken(): string 59 | { 60 | $token = (string)$this->sspSession->getData('form_csrf', 'token'); 61 | 62 | if (!$token) { 63 | $token = Random::generate(); 64 | $this->sspSession->setData('form_csrf', 'token', $token); 65 | } 66 | 67 | return $token ^ $this->sspSession->getSessionId(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Helpers.php: -------------------------------------------------------------------------------- 1 | http(), 34 | ); 35 | } 36 | 37 | public function dateTime(): DateTime 38 | { 39 | return static::$dateTIme ??= new DateTime(); 40 | } 41 | 42 | public function str(): Str 43 | { 44 | return static::$str ??= new Str(); 45 | } 46 | 47 | public function arr(): Arr 48 | { 49 | return static::$arr ??= new Arr(); 50 | } 51 | 52 | public function random(): Random 53 | { 54 | return static::$random ??= new Random(); 55 | } 56 | 57 | public function scope(): Scope 58 | { 59 | return static::$scope ??= new Scope(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Helpers/Arr.php: -------------------------------------------------------------------------------- 1 | (string)$value, $values); 33 | } 34 | 35 | public function isValueOneOf(mixed $value, array $set): bool 36 | { 37 | $value = is_array($value) ? $value : [$value]; 38 | return !empty(array_intersect($value, $set)); 39 | } 40 | 41 | public function isValueSubsetOf(mixed $value, array $superset): bool 42 | { 43 | $value = is_array($value) ? $value : [$value]; 44 | 45 | return empty(array_diff($value, $superset)); 46 | } 47 | 48 | public function isValueSupersetOf(mixed $value, array $subset): bool 49 | { 50 | $value = is_array($value) ? $value : [$value]; 51 | 52 | // Opposite of subset... 53 | return $this->isValueSubsetOf($subset, $value); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Helpers/Client.php: -------------------------------------------------------------------------------- 1 | http->getAllRequestParams($request); 27 | $clientId = empty($params['client_id']) ? null : (string)$params['client_id']; 28 | 29 | if (!is_string($clientId)) { 30 | throw new OidcException('Client ID is missing.'); 31 | } 32 | 33 | $client = $clientRepository->findById($clientId); 34 | 35 | if (!$client) { 36 | throw new OidcException('Client not found.'); 37 | } 38 | 39 | return $client; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Helpers/DateTime.php: -------------------------------------------------------------------------------- 1 | getUtc()->setTimestamp($timestamp); 20 | } 21 | 22 | public function getSecondsToExpirationTime(int $expirationTime): int 23 | { 24 | return $expirationTime - $this->getUtc()->getTimestamp(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Helpers/Http.php: -------------------------------------------------------------------------------- 1 | getQueryParams(), 16 | (is_array($parsedBody = $request->getParsedBody()) ? $parsedBody : []), 17 | ); 18 | } 19 | 20 | /** 21 | * @param \Psr\Http\Message\ServerRequestInterface $request 22 | * @param \SimpleSAML\OpenID\Codebooks\HttpMethodsEnum[] $allowedMethods 23 | * @return ?array 24 | */ 25 | public function getAllRequestParamsBasedOnAllowedMethods( 26 | ServerRequestInterface $request, 27 | array $allowedMethods, 28 | ): ?array { 29 | $requestMethod = HttpMethodsEnum::from(strtoupper($request->getMethod())); 30 | 31 | if (! in_array($requestMethod, $allowedMethods, true)) { 32 | return null; 33 | } 34 | 35 | return match ($requestMethod) { 36 | HttpMethodsEnum::GET => $request->getQueryParams(), 37 | HTTPMethodsEnum::POST => is_array($parsedBody = $request->getParsedBody()) ? $parsedBody : null, 38 | default => null, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Helpers/Random.php: -------------------------------------------------------------------------------- 1 | getIdentifier() === $scopeIdentifier) { 24 | return true; 25 | } 26 | } 27 | 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Helpers/Str.php: -------------------------------------------------------------------------------- 1 | !empty($scope)); 18 | } 19 | 20 | /** 21 | * @param non-empty-string $pattern 22 | * @return string[] 23 | */ 24 | public function convertTextToArray(string $text, string $pattern = "/[\t\r\n]+/"): array 25 | { 26 | return array_filter( 27 | preg_split($pattern, $text), 28 | fn(string $line): bool => !empty(trim($line)), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Repositories/AbstractDatabaseRepository.php: -------------------------------------------------------------------------------- 1 | getTableName()) ? 38 | $tableName . '_' . $identifier : 39 | $identifier; 40 | } 41 | 42 | abstract public function getTableName(): ?string; 43 | } 44 | -------------------------------------------------------------------------------- /src/Repositories/CodeChallengeVerifiersRepository.php: -------------------------------------------------------------------------------- 1 | codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier; 26 | } 27 | 28 | $plainVerifier = new PlainVerifier(); 29 | $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier; 30 | } 31 | 32 | /** 33 | * @return \League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface[] 34 | */ 35 | public function getAll(): array 36 | { 37 | return $this->codeChallengeVerifiers; 38 | } 39 | 40 | /** 41 | * @return \League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface|null 42 | * Verifier for the method or null if not supported. 43 | */ 44 | public function get(string $method): ?CodeChallengeVerifierInterface 45 | { 46 | return $this->codeChallengeVerifiers[$method] ?? null; 47 | } 48 | 49 | public function has(string $method): bool 50 | { 51 | return isset($this->codeChallengeVerifiers[$method]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Repositories/Interfaces/AccessTokenRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * @copyright (c) 2018 Steve Rhoades 15 | * @license http://opensource.org/licenses/MIT MIT 16 | */ 17 | interface IdentityProviderInterface extends RepositoryInterface 18 | { 19 | public function getUserEntityByIdentifier(string $identifier): ?UserEntityInterface; 20 | } 21 | -------------------------------------------------------------------------------- /src/Repositories/Interfaces/RefreshTokenRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | clientId; 25 | } 26 | 27 | public function setClientId(string $clientId): void 28 | { 29 | $this->clientId = $clientId; 30 | } 31 | 32 | public function getUserId(): string 33 | { 34 | return $this->userId; 35 | } 36 | 37 | public function setUserId(string $userId): void 38 | { 39 | $this->userId = $userId; 40 | } 41 | 42 | public function getSessionId(): ?string 43 | { 44 | return $this->sessionId; 45 | } 46 | 47 | public function setSessionId(?string $sessionId): void 48 | { 49 | $this->sessionId = $sessionId; 50 | } 51 | 52 | public function getBackChannelLogoutUri(): ?string 53 | { 54 | return $this->backChannelLogoutUri; 55 | } 56 | 57 | public function setBackChannelLogoutUri(?string $backChannelLogoutUri): void 58 | { 59 | $this->backChannelLogoutUri = $backChannelLogoutUri; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Server/Grants/Interfaces/AuthorizationValidatableWithRequestRules.php: -------------------------------------------------------------------------------- 1 | key; 22 | } 23 | 24 | public function getValue(): mixed 25 | { 26 | return $this->value; 27 | } 28 | 29 | public function setValue(mixed $value): void 30 | { 31 | $this->value = $value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Server/RequestRules/ResultBag.php: -------------------------------------------------------------------------------- 1 | results[$result->getKey()] = $result; 26 | } 27 | 28 | /** 29 | * @param string $key 30 | * @return \SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultInterface|null 31 | */ 32 | public function get(string $key): ?ResultInterface 33 | { 34 | return $this->results[$key] ?? null; 35 | } 36 | 37 | /** 38 | * @param string $key 39 | * @return \SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultInterface 40 | */ 41 | public function getOrFail(string $key): ResultInterface 42 | { 43 | $result = $this->get($key); 44 | 45 | if ($result === null) { 46 | throw new LogicException( 47 | sprintf('Request rule error: expected existing result, but none found (%s)', $key), 48 | ); 49 | } 50 | 51 | return $result; 52 | } 53 | 54 | /** 55 | * @return \SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultInterface[] 56 | */ 57 | public function getAll(): array 58 | { 59 | return $this->results; 60 | } 61 | 62 | /** 63 | * @param string $key 64 | */ 65 | public function remove(string $key): void 66 | { 67 | unset($this->results[$key]); 68 | } 69 | 70 | public function has(string $key): bool 71 | { 72 | return array_key_exists($key, $this->results); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/AbstractRule.php: -------------------------------------------------------------------------------- 1 | getOrFail(ResponseTypeRule::class)->getValue(); 30 | 31 | return new Result($this->getKey(), $responseType === "id_token"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/CodeVerifierRule.php: -------------------------------------------------------------------------------- 1 | getOrFail(ClientIdRule::class)->getValue(); 31 | 32 | $codeVerifier = $this->requestParamsResolver->getFromRequestBasedOnAllowedMethods( 33 | ParamsEnum::CodeVerifier->value, 34 | $request, 35 | $allowedServerRequestMethods, 36 | ); 37 | 38 | if (is_null($codeVerifier)) { 39 | if (!$client->isConfidential()) { 40 | throw OidcServerException::invalidRequest( 41 | 'code_verifier', 42 | 'Code Verifier must be provided for public clients.', 43 | ); 44 | } 45 | 46 | return new Result($this->getKey(), null); 47 | } 48 | 49 | // Validate code_verifier according to RFC-7636 50 | // @see: https://tools.ietf.org/html/rfc7636#section-4.1 51 | if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) { 52 | throw OidcServerException::invalidRequest( 53 | 'code_verifier', 54 | 'Code Verifier must follow the specifications of RFC-7636.', 55 | ); 56 | } 57 | 58 | return new Result($this->getKey(), $codeVerifier); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/RedirectUriRule.php: -------------------------------------------------------------------------------- 1 | getOrFail(ClientIdRule::class)->getValue(); 33 | if (! $client instanceof ClientEntityInterface) { 34 | throw new LogicException('Can not check redirect_uri, client is not ClientEntityInterface.'); 35 | } 36 | 37 | $redirectUri = $this->requestParamsResolver->getAsStringBasedOnAllowedMethods( 38 | ParamsEnum::RedirectUri->value, 39 | $request, 40 | $allowedServerRequestMethods, 41 | ); 42 | 43 | // On OAuth2 redirect_uri is optional if there is only one registered, however we will always require it 44 | // since this is OIDC oriented package and in OIDC this parameter is required. 45 | if ($redirectUri === null) { 46 | throw OidcServerException::invalidRequest(ParamsEnum::RedirectUri->value); 47 | } 48 | 49 | $clientRedirectUri = $client->getRedirectUri(); 50 | if (is_string($clientRedirectUri) && (strcmp($clientRedirectUri, $redirectUri) !== 0)) { 51 | throw OidcServerException::invalidClient($request); 52 | } elseif ( 53 | is_array($clientRedirectUri) && 54 | in_array($redirectUri, $clientRedirectUri, true) === false 55 | ) { 56 | throw OidcServerException::invalidRequest(ParamsEnum::RedirectUri->value); 57 | } 58 | 59 | return new Result($this->getKey(), $redirectUri); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/RequiredNonceRule.php: -------------------------------------------------------------------------------- 1 | getOrFail(RedirectUriRule::class)->getValue(); 32 | /** @var string|null $state */ 33 | $state = $currentResultBag->getOrFail(StateRule::class)->getValue(); 34 | 35 | $nonce = $this->requestParamsResolver->getAsStringBasedOnAllowedMethods( 36 | ParamsEnum::Nonce->value, 37 | $request, 38 | $allowedServerRequestMethods, 39 | ); 40 | 41 | if ($nonce === null || $nonce === '') { 42 | throw OidcServerException::invalidRequest( 43 | ParamsEnum::Nonce->value, 44 | 'nonce is required', 45 | null, 46 | $redirectUri, 47 | $state, 48 | $useFragmentInHttpErrorResponses, 49 | ); 50 | } 51 | 52 | return new Result($this->getKey(), $nonce); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/RequiredOpenIdScopeRule.php: -------------------------------------------------------------------------------- 1 | getOrFail(RedirectUriRule::class)->getValue(); 31 | /** @var string|null $state */ 32 | $state = $currentResultBag->getOrFail(StateRule::class)->getValue(); 33 | /** @var \League\OAuth2\Server\Entities\ScopeEntityInterface[] $validScopes */ 34 | $validScopes = $currentResultBag->getOrFail(ScopeRule::class)->getValue(); 35 | 36 | $isOpenIdScopePresent = (bool) array_filter( 37 | $validScopes, 38 | fn($scopeEntity) => $scopeEntity->getIdentifier() === 'openid', 39 | ); 40 | 41 | if (! $isOpenIdScopePresent) { 42 | throw OidcServerException::invalidRequest( 43 | 'scope', 44 | 'Scope openid is required', 45 | null, 46 | $redirectUri, 47 | $state, 48 | $useFragmentInHttpErrorResponses, 49 | ); 50 | } 51 | 52 | return new Result($this->getKey(), true); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/ResponseTypeRule.php: -------------------------------------------------------------------------------- 1 | requestParamsResolver->getAllBasedOnAllowedMethods( 30 | $request, 31 | $allowedServerRequestMethods, 32 | ); 33 | 34 | if ( 35 | !isset($requestParams[ParamsEnum::ResponseType->value]) || 36 | !isset($requestParams[ParamsEnum::ClientId->value]) 37 | ) { 38 | throw OidcServerException::invalidRequest('Missing response_type or client_id'); 39 | } 40 | 41 | // TODO v7 consider checking for supported response types, for example, from configuration... 42 | 43 | return new Result($this->getKey(), $requestParams[ParamsEnum::ResponseType->value]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/ScopeOfflineAccessRule.php: -------------------------------------------------------------------------------- 1 | getOrFail(RedirectUriRule::class)->getValue(); 31 | /** @var string|null $state */ 32 | $state = $currentResultBag->getOrFail(StateRule::class)->getValue(); 33 | /** @var \SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface $client */ 34 | $client = $currentResultBag->getOrFail(ClientIdRule::class)->getValue(); 35 | /** @var \League\OAuth2\Server\Entities\ScopeEntityInterface[] $validScopes */ 36 | $validScopes = $currentResultBag->getOrFail(ScopeRule::class)->getValue(); 37 | 38 | // Check if offline_access scope is used. If not, we don't have to check anything else. 39 | if (! $this->helpers->scope()->exists($validScopes, 'offline_access')) { 40 | return new Result($this->getKey(), false); 41 | } 42 | 43 | // Scope offline_access is used. Check if the client has it registered. 44 | if (! in_array('offline_access', $client->getScopes(), true)) { 45 | throw OidcServerException::invalidRequest( 46 | 'scope', 47 | 'offline_access scope is not registered for the client', 48 | null, 49 | $redirectUri, 50 | $state, 51 | $useFragmentInHttpErrorResponses, 52 | ); 53 | } 54 | 55 | return new Result($this->getKey(), true); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/StateRule.php: -------------------------------------------------------------------------------- 1 | requestParamsResolver->getAsStringBasedOnAllowedMethods( 29 | ParamsEnum::State->value, 30 | $request, 31 | $allowedServerRequestMethods, 32 | ); 33 | 34 | return new Result($this->getKey(), $state); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Server/RequestRules/Rules/UiLocalesRule.php: -------------------------------------------------------------------------------- 1 | getKey(), $this->requestParamsResolver->getBasedOnAllowedMethods( 29 | ParamsEnum::UiLocales->value, 30 | $request, 31 | $allowedServerRequestMethods, 32 | )); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Server/ResponseTypes/Interfaces/AcrResponseTypeInterface.php: -------------------------------------------------------------------------------- 1 | psrHttpBridge->getHttpFoundationFactory()->createResponse( 34 | $exception->generateHttpResponse($this->psrHttpBridge->getResponseFactory()->createResponse()), 35 | ); 36 | } 37 | 38 | return new JsonResponse( 39 | [ 40 | 'error' => [ 41 | 'code' => 500, 42 | 'message' => $exception->getMessage(), 43 | ], 44 | ], 45 | 500, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/LogoutTokenBuilder.php: -------------------------------------------------------------------------------- 1 | jsonWebTokenBuilderService 25 | ->getProtocolJwtBuilder() 26 | ->withHeader('typ', 'logout+jwt') 27 | ->permittedFor($relyingPartyAssociation->getClientId()) 28 | ->relatedTo($relyingPartyAssociation->getUserId()) 29 | ->withClaim('events', ['http://schemas.openid.net/event/backchannel-logout' => new stdClass()]) 30 | ; 31 | 32 | if ($relyingPartyAssociation->getSessionId() !== null) { 33 | $logoutTokenBuilder = $logoutTokenBuilder->withClaim('sid', $relyingPartyAssociation->getSessionId()); 34 | } 35 | 36 | return $this->jsonWebTokenBuilderService->getSignedProtocolJwt($logoutTokenBuilder)->toString(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Services/SessionMessagesService.php: -------------------------------------------------------------------------------- 1 | session->setData('message', uniqid(), $value); 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function getMessages(): array 39 | { 40 | /** @var array $messages */ 41 | $messages = $this->session->getDataOfType('message'); 42 | 43 | foreach (array_keys($messages) as $key) { 44 | $this->session->deleteData('message', $key); 45 | } 46 | 47 | return $messages; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Services/StateService.php: -------------------------------------------------------------------------------- 1 | authState = new State(); 25 | } 26 | 27 | /** 28 | * @return State 29 | */ 30 | public function getAuthState(): State 31 | { 32 | return $this->authState; 33 | } 34 | 35 | /** 36 | * @param string $id 37 | * @param string $stage 38 | * @param bool $allowMissing 39 | * 40 | * @return array|null 41 | * @throws \SimpleSAML\Error\NoState 42 | */ 43 | public function loadState(string $id, string $stage, bool $allowMissing = false): ?array 44 | { 45 | return $this->authState::loadState($id, $stage, $allowMissing); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Stores/Session/LogoutTicketStoreBuilder.php: -------------------------------------------------------------------------------- 1 | newInstance(...$args); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Utils/FederationCache.php: -------------------------------------------------------------------------------- 1 | getSignedJwksUri()) && 23 | ($federationJwks = $client->getFederationJwks()) 24 | ) { 25 | return $this->jwks->jwksFetcher()->fromCacheOrSignedJwksUri( 26 | $signedJwksUri, 27 | $federationJwks, 28 | )?->jsonSerialize(); 29 | } 30 | 31 | if (($jwksUri = $client->getJwksUri())) { 32 | return $this->jwks->jwksFetcher()->fromCacheOrJwksUri($jwksUri)?->jsonSerialize(); 33 | } 34 | 35 | return $client->getJwks(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Utils/ProtocolCache.php: -------------------------------------------------------------------------------- 1 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 | {% if showMenu %} 15 | {%- include "@admin/includes/menu.twig" %} 16 | {% endif %} 17 | 18 | {% if showModuleName %} 19 |

{{ moduleName }}

20 | {% endif %} 21 | 22 |
23 | 24 | {% if showMenu %} 25 | {% include '@oidc/includes/menu.twig' %} 26 | {% endif %} 27 | 28 |
29 | {% if showSubpageTitle %} 30 |

{{ subPageTitle }}

31 | {% endif %} 32 | 33 | {% if sessionMessages is defined and sessionMessages is not empty %} 34 |
35 | {% for message in sessionMessages %} 36 | {{ message|trans }}
37 | {% endfor %} 38 |
39 | {% endif %} 40 | 41 | {% block oidcContent %}{% endblock %} 42 |
43 |
44 | 45 | {% endblock content -%} 46 | 47 | {% block postload %} 48 | 49 | {{ parent() }} 50 | 51 | 52 | {% endblock postload %} 53 | 54 | {% block oidcPostload %}{% endblock %} 55 | -------------------------------------------------------------------------------- /templates/clients/add.twig: -------------------------------------------------------------------------------- 1 | {% set subPageTitle = 'Add Client'|trans %} 2 | 3 | {% extends "@oidc/base.twig" %} 4 | 5 | {% block oidcContent %} 6 | 7 |
8 |
9 | 10 |
11 | 19 |
20 | 21 | {% include "@oidc/clients/includes/form.twig" %} 22 | 23 | {% endblock oidcContent -%} 24 | 25 | {% block postload %} 26 | {{ parent() }} 27 | 28 | 29 | {% endblock %} -------------------------------------------------------------------------------- /templates/clients/edit.twig: -------------------------------------------------------------------------------- 1 | {% set subPageTitle = 'Edit Client '|trans ~ originalClient.getIdentifier %} 2 | 3 | {% extends "@oidc/base.twig" %} 4 | 5 | {% block oidcContent %} 6 | 7 |
8 |
9 | 10 |
11 | 19 |
20 | 21 | {% include "@oidc/clients/includes/form.twig" %} 22 | 23 | {% endblock oidcContent -%} 24 | 25 | {% block postload %} 26 | {{ parent() }} 27 | 28 | 29 | {% endblock %} -------------------------------------------------------------------------------- /templates/config/migrations.twig: -------------------------------------------------------------------------------- 1 | {% set subPageTitle = 'Database Migrations'|trans %} 2 | 3 | {% extends "@oidc/base.twig" %} 4 | 5 | {% block oidcContent %} 6 | 7 | {% if databaseMigration.isMigrated %} 8 |

{{ 'All database migrations are implemented.'|trans }}

9 | {% else %} 10 |

11 | 12 | {% trans %}There are database migrations that have not been implemented. 13 | Use the button below to run them now.{% endtrans %} 14 |

15 | 16 |
17 | 18 | 19 | 20 |
21 |
22 | {% endif %} 23 | 24 |
25 | {{ 'Before running the migrations, make sure that the database user has proper privileges to change the scheme (for example, alter, create, drop, index). After running the migrations, it is a good practice to remove those privileges.'|trans }} 26 |
27 | 28 | {% endblock oidcContent -%} 29 | -------------------------------------------------------------------------------- /templates/includes/menu.twig: -------------------------------------------------------------------------------- 1 | {% if oidcMenu|default %} 2 | 13 | 14 | 15 | {% endif %} -------------------------------------------------------------------------------- /templates/logout.twig: -------------------------------------------------------------------------------- 1 | {% extends "@oidc/base.twig" %} 2 | 3 | {% set pagetitle = 'Logout Info' | trans %} 4 | 5 | {% block oidcContent %} 6 |

7 | {% if wasLogoutActionCalled %} 8 | {{ 'Logout Successful'|trans }} 9 | {% else %} 10 | {{ 'Logout Failed'|trans }} 11 | {% endif %} 12 |

13 | 14 | 15 |
16 |

17 | {{ 'Info'|trans }} 18 |

19 |

20 | {% if wasLogoutActionCalled %} 21 | {{ 'You can now close this window or navigate to another page.'|trans }} 22 | {% else %} 23 | {{ 'Requested session was not found or it is expired.'|trans }} 24 | {% endif %} 25 |

26 |
27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | hrefPath = 'path'; 21 | $this->label = 'label'; 22 | $this->iconAssetPath = 'icon-path'; 23 | } 24 | 25 | protected function sut( 26 | ?string $hrefPath = null, 27 | ?string $label = null, 28 | ?string $iconAssetPath = null, 29 | ): Item { 30 | $hrefPath ??= $this->hrefPath; 31 | $label ??= $this->label; 32 | $iconAssetPath ??= $this->iconAssetPath; 33 | 34 | return new Item($hrefPath, $label, $iconAssetPath); 35 | } 36 | 37 | public function testCanCreateInstance(): void 38 | { 39 | $sut = $this->sut(); 40 | $this->assertInstanceOf(Item::class, $sut); 41 | 42 | $this->assertSame($sut->getHrefPath(), $this->hrefPath); 43 | $this->assertSame($sut->getLabel(), $this->label); 44 | $this->assertSame($sut->getIconAssetPath(), $this->iconAssetPath); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/unit/src/Admin/MenuTest.php: -------------------------------------------------------------------------------- 1 | itemMock = $this->createMock(Item::class); 23 | } 24 | 25 | protected function sut( 26 | ?Item ...$items, 27 | ): Menu { 28 | return new Menu(...$items); 29 | } 30 | 31 | public function testCanCreateInstance(): void 32 | { 33 | $this->assertInstanceOf(Menu::class, $this->sut()); 34 | $this->assertInstanceOf(Menu::class, $this->sut($this->itemMock)); 35 | } 36 | 37 | public function testCanAddGetItem(): void 38 | { 39 | $sut = $this->sut(); 40 | $this->assertEmpty($sut->getItems()); 41 | $sut->addItem($this->itemMock); 42 | $this->assertCount(1, $sut->getItems()); 43 | } 44 | 45 | public function testCanSetGetActiveHrefPath(): void 46 | { 47 | $sut = $this->sut(); 48 | $this->assertNull($sut->getActiveHrefPath()); 49 | $sut->setActiveHrefPath('oidc'); 50 | $this->assertSame('oidc', $sut->getActiveHrefPath()); 51 | } 52 | 53 | public function testCanBuildItem(): void 54 | { 55 | $this->assertInstanceOf(Item::class, $this->sut()->buildItem('oidc', 'OIDC')); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/src/Bridges/SspBridge/Auth/SourceTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(in_array('admin', $this->sut()->getSources())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/src/Bridges/SspBridge/AuthTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Auth::class, $this->sut()); 22 | } 23 | 24 | public function testCanBuildSourceInstance(): void 25 | { 26 | $this->assertInstanceOf(Auth\Source::class, $this->sut()->source()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/src/Bridges/SspBridge/Module/AdminTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Admin::class, $this->sut()); 23 | } 24 | 25 | public function testCanBuildSspAdminMenu(): void 26 | { 27 | $this->assertInstanceOf(Menu::class, $this->sut()->buildSspAdminMenu()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/unit/src/Bridges/SspBridge/ModuleTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Module::class, $this->sut()); 22 | } 23 | 24 | public function testCanBuildAdminInstance(): void 25 | { 26 | $this->assertInstanceOf(Module\Admin::class, $this->sut()->admin()); 27 | } 28 | 29 | public function testCanGetModuleUrl(): void 30 | { 31 | $this->assertStringContainsString( 32 | 'test', 33 | $this->sut()->getModuleUrl('test'), 34 | ); 35 | } 36 | 37 | public function testCanCheckIsModuleEnabled(): void 38 | { 39 | $this->assertFalse($this->sut()->isModuleEnabled('invalid')); 40 | $this->assertTrue($this->sut()->isModuleEnabled('core')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/src/Bridges/SspBridge/UtilsTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Utils::class, $this->sut()); 27 | } 28 | 29 | public function testCanBuildConfigInstance(): void 30 | { 31 | $this->assertInstanceOf(Config::class, $this->sut()->config()); 32 | } 33 | 34 | public function testCanBuildHttpInstance(): void 35 | { 36 | $this->assertInstanceOf(HTTP::class, $this->sut()->http()); 37 | } 38 | 39 | public function testCanBuildRandomInstance(): void 40 | { 41 | $this->assertInstanceOf(Random::class, $this->sut()->random()); 42 | } 43 | 44 | public function testCanBuildAuthInstance(): void 45 | { 46 | $this->assertInstanceOf(Auth::class, $this->sut()->auth()); 47 | } 48 | 49 | public function testCanBuileAttributesInstance(): void 50 | { 51 | $this->assertInstanceOf(Attributes::class, $this->sut()->attributes()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/unit/src/Bridges/SspBridgeTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SspBridge::class, $this->sut()); 22 | } 23 | 24 | public function testCanBuildUtilsInstance(): void 25 | { 26 | $this->assertInstanceOf(SspBridge\Utils::class, $this->sut()->utils()); 27 | } 28 | 29 | public function testCanBuildModuleInstance(): void 30 | { 31 | $this->assertInstanceOf(SspBridge\Module::class, $this->sut()->module()); 32 | } 33 | 34 | public function testCanBuildAuthInstance(): void 35 | { 36 | $this->assertInstanceOf(SspBridge\Auth::class, $this->sut()->auth()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/unit/src/Codebooks/RegistrationTypeEnumTest.php: -------------------------------------------------------------------------------- 1 | assertStringContainsString( 17 | 'Manual', 18 | RegistrationTypeEnum::Manual->description(), 19 | ); 20 | 21 | $this->assertStringContainsString( 22 | 'Automatic', 23 | RegistrationTypeEnum::FederatedAutomatic->description(), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/src/Controllers/JwksControllerTest.php: -------------------------------------------------------------------------------- 1 | jsonWebKeySetServiceMock = $this->createMock(JsonWebKeySetService::class); 29 | $this->serverRequestMock = $this->createMock(ServerRequest::class); 30 | $this->psrHttpBridge = $this->createMock(PsrHttpBridge::class); 31 | } 32 | 33 | protected function mock(): JwksController 34 | { 35 | return new JwksController( 36 | $this->jsonWebKeySetServiceMock, 37 | $this->psrHttpBridge, 38 | ); 39 | } 40 | 41 | public function testItIsInitializable(): void 42 | { 43 | $this->assertInstanceOf( 44 | JwksController::class, 45 | $this->mock(), 46 | ); 47 | } 48 | 49 | public function testItReturnsJsonKeys(): void 50 | { 51 | $keys = [ 52 | 0 => [ 53 | 'kty' => 'RSA', 54 | 'n' => 'n', 55 | 'e' => 'e', 56 | 'use' => 'sig', 57 | 'kid' => 'oidc', 58 | 'alg' => 'RS256', 59 | ], 60 | ]; 61 | 62 | $this->jsonWebKeySetServiceMock->expects($this->once())->method('protocolKeys')->willReturn($keys); 63 | 64 | $this->assertSame( 65 | ['keys' => $keys], 66 | $this->mock()->__invoke()->getPayload(), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/unit/src/Entities/ClaimSetEntityTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ClaimSetEntity::class, $sut); 19 | $this->assertSame('scope', $sut->getScope()); 20 | $this->assertSame(['claim'], $sut->getClaims()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/src/Entities/ScopeEntityTest.php: -------------------------------------------------------------------------------- 1 | 'attrval'], 17 | ): ScopeEntity { 18 | return new ScopeEntity($id, $description, $icon, $attributes); 19 | } 20 | 21 | public function testItIsInitializable(): void 22 | { 23 | $this->assertInstanceOf( 24 | ScopeEntity::class, 25 | $this->mock(), 26 | ); 27 | } 28 | 29 | public function testCanGetProperties(): void 30 | { 31 | $scopeEntity = $this->mock(); 32 | $this->assertSame('id', $scopeEntity->getIdentifier()); 33 | $this->assertSame('description', $scopeEntity->getDescription()); 34 | $this->assertSame('icon', $scopeEntity->getIcon()); 35 | $this->assertSame(['attrid' => 'attrval'], $scopeEntity->getClaims()); 36 | $this->assertSame('id', $scopeEntity->jsonSerialize()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/unit/src/Factories/AuthSimpleFactoryTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/unit/src/Factories/FormFactoryTest.php: -------------------------------------------------------------------------------- 1 | moduleConfigMock = $this->createMock(ModuleConfig::class); 30 | $this->csrfProtectionMock = $this->createMock(CsrfProtection::class); 31 | $this->sspBridgeMock = $this->createMock(SspBridge::class); 32 | $this->helpersMock = $this->createMock(Helpers::class); 33 | } 34 | 35 | protected function sut( 36 | ?ModuleConfig $moduleConfig = null, 37 | ?CsrfProtection $csrfProtection = null, 38 | ?SspBridge $sspBridge = null, 39 | ?Helpers $helpers = null, 40 | ): FormFactory { 41 | $moduleConfig ??= $this->moduleConfigMock; 42 | $csrfProtection ??= $this->csrfProtectionMock; 43 | $sspBridge ??= $this->sspBridgeMock; 44 | $helpers ??= $this->helpersMock; 45 | 46 | return new FormFactory( 47 | $moduleConfig, 48 | $csrfProtection, 49 | $sspBridge, 50 | $helpers, 51 | ); 52 | } 53 | 54 | public function testCanConstruct(): void 55 | { 56 | $this->assertInstanceOf(FormFactory::class, $this->sut()); 57 | } 58 | 59 | public function testCanBuildClientForm(): void 60 | { 61 | $this->assertInstanceOf( 62 | ClientForm::class, 63 | $this->sut()->build(ClientForm::class), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/unit/src/Helpers/ArrTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 22 | 'a', 23 | $this->sut()->findByCallback( 24 | ['a', 'b', 'c'], 25 | fn($item): bool => $item === 'a', 26 | ), 27 | ); 28 | 29 | $this->assertNull($this->sut()->findByCallback( 30 | ['a', 'b', 'c'], 31 | fn($item): bool => $item === 'd', 32 | )); 33 | } 34 | 35 | public function testEnsureStringValues(): void 36 | { 37 | $this->assertSame( 38 | ['1', '2'], 39 | $this->sut()->ensureStringValues([1, 2]), 40 | ); 41 | } 42 | 43 | public function testIsValueOneOf(): void 44 | { 45 | $this->assertTrue($this->sut()->isValueOneOf('a', ['a'])); 46 | $this->assertTrue($this->sut()->isValueOneOf(['a'], ['a'])); 47 | $this->assertTrue($this->sut()->isValueOneOf(['a', 'b'], ['a'])); 48 | 49 | $this->assertFalse($this->sut()->isValueOneOf('a', ['b'])); 50 | $this->assertFalse($this->sut()->isValueOneOf(['a'], ['b'])); 51 | } 52 | 53 | public function testIsValueSubsetOf(): void 54 | { 55 | $this->assertTrue($this->sut()->isValueSubsetOf('a', ['a', 'b', 'c'])); 56 | $this->assertTrue($this->sut()->isValueSubsetOf(['a'], ['a', 'b', 'c'])); 57 | $this->assertTrue($this->sut()->isValueSubsetOf(['a', 'b'], ['a', 'b', 'c'])); 58 | 59 | $this->assertFalse($this->sut()->isValueSubsetOf('a', [])); 60 | $this->assertFalse($this->sut()->isValueSubsetOf('a', ['b'])); 61 | $this->assertFalse($this->sut()->isValueSubsetOf(['a', 'c'], ['b'])); 62 | } 63 | 64 | public function testIsValueSupersetOf(): void 65 | { 66 | $this->assertTrue($this->sut()->isValueSupersetOf('a', ['a'])); 67 | $this->assertTrue($this->sut()->isValueSupersetOf(['a'], ['a'])); 68 | $this->assertTrue($this->sut()->isValueSupersetOf(['a', 'b'], ['a'])); 69 | 70 | $this->assertFalse($this->sut()->isValueSupersetOf('a', ['b'])); 71 | $this->assertFalse($this->sut()->isValueSupersetOf(['a'], ['b'])); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/unit/src/Helpers/DateTimeTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\DateTimeImmutable::class, $this->sut()->getUtc()); 23 | $this->assertSame( 24 | 'UTC', 25 | $this->sut()->getUtc()->getTimezone()->getName(), 26 | ); 27 | } 28 | 29 | public function testCanGetFromTimestamp(): void 30 | { 31 | $timestamp = (new DateTimeImmutable())->getTimestamp(); 32 | 33 | $this->assertSame( 34 | $timestamp, 35 | $this->sut()->getFromTimestamp($timestamp)->getTimestamp(), 36 | ); 37 | } 38 | 39 | public function testCanGetSecondsToExpirationTime(): void 40 | { 41 | $expirationTime = (new DateTimeImmutable())->getTimestamp() + 60; 42 | 43 | $this->assertSame( 44 | 60, 45 | $this->sut()->getSecondsToExpirationTime($expirationTime), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/src/Helpers/RandomTest.php: -------------------------------------------------------------------------------- 1 | assertNotEmpty( 22 | $this->sut()->getIdentifier(), 23 | ); 24 | } 25 | 26 | public function testGetIdentifierThrowsOnInvalidLength(): void 27 | { 28 | $this->expectException(OidcServerException::class); 29 | $this->expectExceptionMessage('Random'); 30 | 31 | $this->sut()->getIdentifier(0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/src/Helpers/ScopeTest.php: -------------------------------------------------------------------------------- 1 | scopeEntityOpenIdStub = $this->createStub(ScopeEntityInterface::class); 27 | $this->scopeEntityOpenIdStub->method('getIdentifier')->willReturn('openid'); 28 | $this->scopeEntityProfileStub = $this->createStub(ScopeEntityInterface::class); 29 | $this->scopeEntityProfileStub->method('getIdentifier')->willReturn('profile'); 30 | $this->scopeEntitiesArray = [ 31 | $this->scopeEntityOpenIdStub, 32 | $this->scopeEntityProfileStub, 33 | ]; 34 | } 35 | 36 | protected function sut(): Scope 37 | { 38 | return new Scope(); 39 | } 40 | 41 | /** 42 | * @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException 43 | */ 44 | public function testCanCheckScopeExistence(): void 45 | { 46 | $this->assertTrue($this->sut()->exists($this->scopeEntitiesArray, 'openid')); 47 | $this->assertTrue($this->sut()->exists($this->scopeEntitiesArray, 'profile')); 48 | $this->assertFalse($this->sut()->exists($this->scopeEntitiesArray, 'invalid')); 49 | } 50 | 51 | public function testThrowsForInvalidScopeEntity(): void 52 | { 53 | $this->expectException(OidcServerException::class); 54 | 55 | $this->sut()->exists(['invalid'], 'test'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/src/Helpers/StrTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 22 | ['a', 'b'], 23 | $this->sut()->convertScopesStringToArray('a b'), 24 | ); 25 | } 26 | 27 | public function testCanConvertTextToArray(): void 28 | { 29 | $this->assertSame( 30 | ['a', 'b', 'c', 'd'], 31 | $this->sut()->convertTextToArray("a\tb\nc\rd"), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/src/HelpersTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Helpers\Http::class, $this->sut()->http()); 30 | $this->assertInstanceOf(Helpers\Client::class, $this->sut()->client()); 31 | $this->assertInstanceOf(Helpers\DateTime::class, $this->sut()->dateTime()); 32 | $this->assertInstanceOf(Helpers\Str::class, $this->sut()->str()); 33 | $this->assertInstanceOf(Helpers\Arr::class, $this->sut()->arr()); 34 | $this->assertInstanceOf(Helpers\Random::class, $this->sut()->random()); 35 | $this->assertInstanceOf(Helpers\Scope::class, $this->sut()->scope()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/src/Repositories/AbstractDatabaseRepositoryTest.php: -------------------------------------------------------------------------------- 1 | moduleConfigMock = $this->createMock(ModuleConfig::class); 25 | $this->databaseMock = $this->createMock(Database::class); 26 | $this->protocolCacheMock = $this->createMock(ProtocolCache::class); 27 | } 28 | 29 | protected function sut( 30 | ?ModuleConfig $moduleConfig = null, 31 | ?Database $database = null, 32 | ?ProtocolCache $protocolCache = null, 33 | ): AbstractDatabaseRepository { 34 | $moduleConfig ??= $this->moduleConfigMock; 35 | $database ??= $this->databaseMock; 36 | $protocolCache ??= $this->protocolCacheMock; 37 | 38 | return new class ($moduleConfig, $database, $protocolCache) extends AbstractDatabaseRepository 39 | { 40 | public function getTableName(): ?string 41 | { 42 | return 'sut'; 43 | } 44 | }; 45 | } 46 | 47 | public function testCanGetCacheKey(): void 48 | { 49 | $this->assertSame('sut_something', $this->sut()->getCacheKey('something')); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/unit/src/Repositories/CodeChallengeVerifiersRepositoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(CodeChallengeVerifiersRepository::class, $this->sut()); 23 | } 24 | 25 | public function testCanGetCodeChallengeVerifier(): void 26 | { 27 | $this->assertInstanceOf( 28 | CodeChallengeVerifierInterface::class, 29 | $this->sut()->get('S256'), 30 | ); 31 | $this->assertTrue($this->sut()->has('S256')); 32 | 33 | $this->assertInstanceOf( 34 | CodeChallengeVerifierInterface::class, 35 | $this->sut()->get('plain'), 36 | ); 37 | $this->assertTrue($this->sut()->has('plain')); 38 | 39 | $this->assertNotEmpty($this->sut()->getAll()); 40 | } 41 | 42 | public function testReturnsNullForUnsuportedVerifier(): void 43 | { 44 | $this->assertNull($this->sut()->get('unsuported')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/unit/src/Server/Associations/RelyingPartyAssociationTest.php: -------------------------------------------------------------------------------- 1 | clientId, 24 | $this->userId, 25 | $this->sessionId, 26 | $this->backChannelLogoutUri, 27 | ); 28 | 29 | $this->assertEquals($this->clientId, $rpAssociation->getClientId()); 30 | $this->assertEquals($this->userId, $rpAssociation->getUserId()); 31 | $this->assertEquals($this->sessionId, $rpAssociation->getSessionId()); 32 | $this->assertEquals($this->backChannelLogoutUri, $rpAssociation->getBackChannelLogoutUri()); 33 | 34 | $newClientId = 'newClient123'; 35 | $newUserId = 'newUser123'; 36 | $newSessionId = 'newSession123'; 37 | $newBackChannelLogoutUri = 'https//new.example.org/logout'; 38 | 39 | $rpAssociation->setClientId($newClientId); 40 | $rpAssociation->setUserId($newUserId); 41 | $rpAssociation->setSessionId($newSessionId); 42 | $rpAssociation->setBackChannelLogoutUri($newBackChannelLogoutUri); 43 | 44 | $this->assertEquals($newClientId, $rpAssociation->getClientId()); 45 | $this->assertEquals($newUserId, $rpAssociation->getUserId()); 46 | $this->assertEquals($newSessionId, $rpAssociation->getSessionId()); 47 | $this->assertEquals($newBackChannelLogoutUri, $rpAssociation->getBackChannelLogoutUri()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/unit/src/Server/AuthorizationServerTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/unit/src/Server/RequestRules/ResultBagTest.php: -------------------------------------------------------------------------------- 1 | result = new Result($this->key, $this->value); 27 | $this->resultBag = new ResultBag(); 28 | } 29 | 30 | public function testGetAll(): void 31 | { 32 | $this->assertEmpty($this->resultBag->getAll()); 33 | $this->resultBag->add($this->result); 34 | $this->resultBag->add(new Result('second', 'second')); 35 | $this->assertCount(2, $this->resultBag->getAll()); 36 | } 37 | 38 | public function testAdd(): void 39 | { 40 | $this->assertNull($this->resultBag->get($this->key)); 41 | $this->resultBag->add($this->result); 42 | $this->assertInstanceOf(Result::class, $this->resultBag->get($this->key)); 43 | } 44 | 45 | public function testGetOrFail(): void 46 | { 47 | $this->resultBag->add($this->result); 48 | $this->assertSame($this->result, $this->resultBag->getOrFail($this->key)); 49 | $this->expectException(LogicException::class); 50 | $this->resultBag->getOrFail('non-existent'); 51 | } 52 | 53 | public function testGet(): void 54 | { 55 | $this->assertNull($this->resultBag->get($this->key)); 56 | $this->resultBag->add($this->result); 57 | $this->assertSame($this->result, $this->resultBag->get($this->key)); 58 | } 59 | 60 | public function testRemove(): void 61 | { 62 | $this->assertNull($this->resultBag->get($this->key)); 63 | $this->resultBag->add($this->result); 64 | $this->assertSame($this->result, $this->resultBag->get($this->key)); 65 | $this->resultBag->remove($this->key); 66 | $this->assertNull($this->resultBag->get($this->key)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/unit/src/Server/RequestRules/ResultTest.php: -------------------------------------------------------------------------------- 1 | key, $this->value); 21 | $this->assertInstanceOf(Result::class, $result); 22 | return $result; 23 | } 24 | 25 | public function testConstructWithoutValue(): void 26 | { 27 | $this->assertInstanceOf(Result::class, new Result($this->key)); 28 | } 29 | 30 | /** 31 | * @depends testConstruct 32 | */ 33 | public function testGetKey(Result $result): void 34 | { 35 | $this->assertSame($this->key, $result->getKey()); 36 | } 37 | 38 | /** 39 | * @depends testConstruct 40 | * 41 | */ 42 | public function testGetValue(Result $result): void 43 | { 44 | $this->assertSame($this->value, $result->getValue()); 45 | } 46 | 47 | /** 48 | * @depends testConstruct 49 | */ 50 | public function testSetValue(Result $result): void 51 | { 52 | $newValue = 'new-value'; 53 | $result->setValue($newValue); 54 | 55 | $this->assertSame($newValue, $result->getValue()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/src/Server/RequestTypes/AuthorizationRequestTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/unit/src/Services/IdTokenBuilderTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/unit/src/Services/SessionMessagesServiceTest.php: -------------------------------------------------------------------------------- 1 | sessionMock = $this->createMock(Session::class); 25 | } 26 | 27 | public function prepareMockedInstance(): SessionMessagesService 28 | { 29 | return new SessionMessagesService($this->sessionMock); 30 | } 31 | 32 | public function testItIsInitializable(): void 33 | { 34 | $this->assertInstanceOf( 35 | SessionMessagesService::class, 36 | $this->prepareMockedInstance(), 37 | ); 38 | } 39 | 40 | /** 41 | * @throws \Exception 42 | */ 43 | public function testItAddsMessage(): void 44 | { 45 | $this->sessionMock->expects($this->once()) 46 | ->method('setData') 47 | ->with('message', $this->anything(), 'value'); 48 | 49 | $this->prepareMockedInstance()->addMessage('value'); 50 | } 51 | 52 | public function testItGetsMessages(): void 53 | { 54 | $this->sessionMock->expects($this->once()) 55 | ->method('getDataOfType') 56 | ->with('message') 57 | ->willReturn( 58 | [ 59 | 'msg1' => 'Message one.', 60 | 'msg2' => 'Message two.', 61 | ], 62 | ); 63 | 64 | $this->sessionMock->expects($this->exactly(2)) 65 | ->method('deleteData') 66 | ->with($this->callback(fn($id) => ! in_array($id, ['msg1', 'msg2']))); 67 | 68 | $this->prepareMockedInstance()->getMessages(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/unit/src/Services/SessionServiceTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/unit/src/Services/StateServiceTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf( 29 | StateService::class, 30 | $this->mock(), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/src/Stores/Session/LogoutTicketStoreBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'sqlite::memory:', 21 | 'database.username' => null, 22 | 'database.password' => null, 23 | 'database.prefix' => 'phpunit_', 24 | 'database.persistent' => true, 25 | 'database.secondaries' => [], 26 | ]; 27 | 28 | Configuration::loadFromArray($config, '', 'simplesaml'); 29 | $builder = new LogoutTicketStoreBuilder(); 30 | 31 | $this->assertInstanceOf(LogoutTicketStoreInterface::class, $builder->getInstance()); 32 | $this->assertInstanceOf(LogoutTicketStoreInterface::class, $builder::getStaticInstance()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/src/Stores/Session/LogoutTicketStoreDbTest.php: -------------------------------------------------------------------------------- 1 | 'sqlite::memory:', 21 | 'database.username' => null, 22 | 'database.password' => null, 23 | 'database.prefix' => 'phpunit_', 24 | 'database.persistent' => true, 25 | 'database.secondaries' => [], 26 | ]; 27 | 28 | Configuration::loadFromArray($config, '', 'simplesaml'); 29 | (new DatabaseMigration())->migrate(); 30 | } 31 | 32 | /** 33 | * @throws \Exception 34 | */ 35 | public function testCanAddAndDeleteTickets(): void 36 | { 37 | $store = new LogoutTicketStoreDb(); 38 | $sid = 'sid123'; 39 | $store->add($sid); 40 | $allSids = $store->getAll(); 41 | $this->assertNotEmpty($allSids); 42 | $this->assertSame($sid, $allSids[0]['sid']); 43 | 44 | $store->delete($sid); 45 | 46 | $this->assertEmpty($store->getAll()); 47 | } 48 | 49 | /** 50 | * @throws \Exception 51 | */ 52 | public function testCanDeleteMultipleTickets(): void 53 | { 54 | $sid1 = 'sid1'; 55 | $sid2 = 'sid2'; 56 | $sid3 = 'sid3'; 57 | 58 | $store = new LogoutTicketStoreDb(); 59 | $store->add($sid1); 60 | $store->add($sid2); 61 | $store->add($sid3); 62 | 63 | $this->assertSame(3, count($store->getAll())); 64 | 65 | $store->deleteMultiple([]); 66 | 67 | $this->assertSame(3, count($store->getAll())); 68 | 69 | $store->deleteMultiple([$sid1, $sid2]); 70 | 71 | $this->assertSame(1, count($store->getAll())); 72 | 73 | $store->delete($sid3); 74 | 75 | $this->assertEmpty($store->getAll()); 76 | } 77 | 78 | /** 79 | * @throws \Exception 80 | */ 81 | public function testCanDeleteExpiredTickets(): void 82 | { 83 | $store = new LogoutTicketStoreDb(null, 0); 84 | $sid = 'sid123'; 85 | $store->add($sid); 86 | $this->assertEmpty($store->getAll()); 87 | } 88 | } 89 | --------------------------------------------------------------------------------