├── .gitignore ├── ci └── qa │ ├── phpmd │ ├── phpcpd │ ├── phplint.yaml │ ├── validate │ ├── phpstan │ ├── docheader │ ├── phpstan.neon │ ├── rector.sh │ ├── phplint │ ├── phpstan-update-baseline │ ├── phpcbf │ ├── phpcs │ ├── phpunit │ ├── rector.php │ ├── docheader.template │ ├── phpunit.xml │ ├── phpcs.xml │ └── phpmd.xml ├── manifest.json ├── src ├── Resources │ ├── keys │ │ ├── README.md │ │ ├── development_publickey.cer │ │ └── development_privatekey.pem │ └── config │ │ └── services_authentication.yaml ├── Exception │ ├── Exception.php │ ├── LogicException.php │ ├── RuntimeException.php │ ├── UnexpectedValueException.php │ ├── NotFound.php │ ├── UnknownUrnException.php │ ├── SamlInvalidConfigurationException.php │ └── InvalidArgumentException.php ├── Security │ ├── Exception │ │ ├── UnexpectedIssuerException.php │ │ ├── LogicException.php │ │ └── RuntimeException.php │ └── Authentication │ │ ├── Handler │ │ ├── SuccessHandler.php │ │ ├── AuthenticationHandler.php │ │ ├── FailureHandler.php │ │ └── ProcessSamlAuthenticationHandler.php │ │ ├── Provider │ │ └── SamlProviderInterface.php │ │ ├── SamlAuthenticationStateHandler.php │ │ ├── Passport │ │ └── Badge │ │ │ └── SamlAttributesBadge.php │ │ ├── Token │ │ └── SamlToken.php │ │ ├── AuthenticatedSessionStateHandler.php │ │ └── SamlInteractionProvider.php ├── Http │ ├── Exception │ │ ├── InvalidRequestException.php │ │ ├── UnsignedRequestException.php │ │ ├── AuthnFailedSamlResponseException.php │ │ ├── InvalidReceivedAuthnRequestPostException.php │ │ ├── NoAuthnContextSamlResponseException.php │ │ ├── SignatureValidationFailedException.php │ │ ├── InvalidReceivedAuthnRequestQueryStringException.php │ │ ├── UnsupportedSignatureException.php │ │ └── UnknownServiceProviderException.php │ ├── SignatureVerifiable.php │ ├── HttpBinding.php │ ├── XMLResponse.php │ ├── HttpBindingFactory.php │ └── ReceivedAuthnRequestPost.php ├── Tests │ ├── Component │ │ ├── Extensions │ │ │ ├── without_extensions.xml │ │ │ ├── written_extensions.xml │ │ │ ├── with_extensions.xml │ │ │ └── ChunkSerializationTest.php │ │ └── Metadata │ │ │ ├── keys │ │ │ ├── idp-cert.pem │ │ │ ├── entity.crt │ │ │ └── entity.key │ │ │ └── XsdValidator.php │ ├── Bootstrap.php │ ├── Unit │ │ ├── SAML2 │ │ │ ├── Resources │ │ │ │ ├── valid-unsigned.xml │ │ │ │ ├── valid-signed-adfs.xml │ │ │ │ ├── invalid-missing-signature-value.xml │ │ │ │ ├── invalid-empty-signature-value.xml │ │ │ │ ├── invalid-malformed-signature-value.xml │ │ │ │ ├── invalid-missing-signing-algorithm.xml │ │ │ │ └── valid-signed.xml │ │ │ ├── Attribute │ │ │ │ ├── Mock │ │ │ │ │ └── DummyAttributeSet.php │ │ │ │ └── ConfigurableAttributeSetFactoryTest.php │ │ │ ├── ReceivedAuthnRequestPostTest.php │ │ │ ├── Response │ │ │ │ └── Assertion │ │ │ │ │ └── InResponseToTest.php │ │ │ └── AuthnRequestFactoryTest.php │ │ ├── Metadata │ │ │ ├── invalid_certificate.pem │ │ │ ├── certificate.pem │ │ │ └── MetadataFactoryTest.php │ │ ├── Security │ │ │ └── FakeAuthencationStateHandler.php │ │ ├── Monolog │ │ │ └── SamlAuthenticationLoggerTest.php │ │ └── Mock │ │ │ └── saml-assertion.txt │ └── TestSaml2Container.php ├── Entity │ ├── ServiceProviderRepository.php │ ├── IdentityProviderRepository.php │ ├── IdentityProvider.php │ ├── ServiceProvider.php │ ├── StaticServiceProviderRepository.php │ ├── StaticIdentityProviderRepository.php │ ├── ImmutableCollection │ │ ├── ServiceProviders.php │ │ └── IdentityProviders.php │ └── HostedEntities.php ├── Signing │ ├── KeyPair.php │ └── Signable.php ├── SAML2 │ ├── Attribute │ │ ├── Filter │ │ │ └── AttributeFilter.php │ │ ├── AttributeSetFactory.php │ │ ├── Attribute.php │ │ ├── AttributeSetInterface.php │ │ ├── ConfigurableAttributeSetFactory.php │ │ ├── AttributeDefinition.php │ │ ├── AttributeSet.php │ │ └── AttributeDictionary.php │ ├── Response │ │ ├── Assertion │ │ │ └── InResponseTo.php │ │ └── AssertionAdapter.php │ ├── Extensions │ │ ├── Extensions.php │ │ ├── Chunk.php │ │ ├── ExtensionsMapperTrait.php │ │ └── GsspUserAttributesChunk.php │ └── BridgeContainer.php ├── Metadata │ ├── MetadataConfiguration.php │ └── Metadata.php ├── SurfnetSamlBundle.php ├── DependencyInjection │ └── Compiler │ │ ├── SamlAttributeRegistrationCompilerPass.php │ │ └── SpRepositoryAliasCompilerPass.php ├── EventSubscriber │ └── BridgeContainerBootListener.php ├── Service │ └── SigningService.php ├── Value │ └── DateTime.php └── Monolog │ └── SamlAuthenticationLogger.php ├── .github └── workflows │ ├── test-integration.yml │ └── daily-security-check.yml ├── templates └── Metadata │ └── metadata.xml.twig ├── composer.json └── UPGRADING.md /.gitignore: -------------------------------------------------------------------------------- 1 | /cache.properties 2 | /vendor/ 3 | .idea 4 | composer.lock 5 | .phpunit.result.cache 6 | var -------------------------------------------------------------------------------- /ci/qa/phpmd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | ./vendor/bin/phpmd src text ci/qa/phpmd.xml 6 | -------------------------------------------------------------------------------- /ci/qa/phpcpd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | # https://github.com/sebastianbergmann/phpcpd 6 | vendor/bin/phpcpd ./src -------------------------------------------------------------------------------- /ci/qa/phplint.yaml: -------------------------------------------------------------------------------- 1 | path: [./src] 2 | jobs: 10 3 | cache-dir: var/qa/phplint.cache 4 | extensions: 5 | - php 6 | exclude: 7 | - vendor 8 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundles": { 3 | "Surfnet\\SamlBundle\\SurfnetSamlBundle": ["all"] 4 | }, 5 | "aliases": ["stepup-saml"] 6 | } 7 | -------------------------------------------------------------------------------- /ci/qa/validate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | # For now only the composer lockfile is checked 6 | composer validate 7 | -------------------------------------------------------------------------------- /ci/qa/phpstan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | vendor/bin/phpstan analyze --memory-limit=-1 --no-ansi -c ./ci/qa/phpstan.neon 6 | -------------------------------------------------------------------------------- /ci/qa/docheader: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | ./vendor/bin/docheader --no-ansi --docheader=ci/qa/docheader.template check src/ 6 | 7 | -------------------------------------------------------------------------------- /ci/qa/phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./phpstan-baseline.neon 3 | parameters: 4 | level: 9 5 | paths: 6 | - ../../src 7 | excludePaths: 8 | - ../../src/Tests 9 | -------------------------------------------------------------------------------- /ci/qa/rector.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Ensure we run from project root 4 | cd "$(dirname "$0")/../../" || exit 1 5 | ./vendor/bin/rector --config=ci/qa/rector.php "$@" 6 | -------------------------------------------------------------------------------- /src/Resources/keys/README.md: -------------------------------------------------------------------------------- 1 | # WARNING 2 | 3 | **THE KEYS DISTRIBUTED WITH THIS BUNDLE ARE FOR DEVELOPMENT PURPOSES ONLY AND SHOULD NEVER BE USED IN ANY OTHER ENVIRONMENT** 4 | -------------------------------------------------------------------------------- /ci/qa/phplint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | mkdir -p var/qa 5 | 6 | # https://github.com/overtrue/phplint 7 | ./vendor/bin/phplint --configuration=ci/qa/phplint.yaml $1 8 | -------------------------------------------------------------------------------- /ci/qa/phpstan-update-baseline: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | vendor/bin/phpstan analyse --memory-limit=-1 --no-ansi -c ./ci/qa/phpstan.neon --generate-baseline ./ci/qa/phpstan-baseline.neon 6 | -------------------------------------------------------------------------------- /ci/qa/phpcbf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | # https://github.com/squizlabs/PHP_CodeSniffer/wiki/Fixing-Errors-Automatically 6 | ./vendor/bin/phpcbf --standard=ci/qa/phpcs.xml --extensions=php src $1 7 | -------------------------------------------------------------------------------- /ci/qa/phpcs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | # https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage 6 | ./vendor/bin/phpcs --report=full --standard=ci/qa/phpcs.xml --warning-severity=0 --extensions=php src 7 | -------------------------------------------------------------------------------- /ci/qa/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0)/../../ 4 | 5 | # PHPUnit Bridge should always be used in Symfony applications. (https://symfony.com/doc/current/components/phpunit_bridge.html) 6 | # This will create a phpunit executable in /bin/ instead of /vendor/bin/ 7 | php ./bin/console cache:clear --env=test 8 | ./vendor/bin/phpunit -c ci/qa/phpunit.xml 9 | -------------------------------------------------------------------------------- /ci/qa/rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/../../src', 10 | ]) 11 | ->withAttributesSets(all: true) 12 | ->withComposerBased(phpunit: true, symfony: true) 13 | ->withTypeCoverageLevel(10) 14 | ->withDeadCodeLevel(10) 15 | ->withCodeQualityLevel(10); 16 | -------------------------------------------------------------------------------- /ci/qa/docheader.template: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright %regexp:\d{4}% SURFnet %regexp:(B.V.|bv)% 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ../../src/Tests/Unit 17 | 18 | 19 | ../../src/Tests/Component 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Security/Exception/UnexpectedIssuerException.php: -------------------------------------------------------------------------------- 1 | 2 | 8 | https://gateway.example.com/gssp/tiqr/metadata 9 | 10 | https://selfservice.stepup.example.com/registration/gssf/tiqr/metadata 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Exception/UnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | get('ssoUrl'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Http/Exception/InvalidReceivedAuthnRequestPostException.php: -------------------------------------------------------------------------------- 1 | get('assertionConsumerUrl'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Resources/valid-unsigned.xml: -------------------------------------------------------------------------------- 1 | http://localhost:8989/simplesaml/module.php/saml/sp/metadata.php/sfo-sp 2 | urn:collab:person:example.org:studenthttp://pilot.surfconext.nl/assurance/sfo-level2 -------------------------------------------------------------------------------- /.github/workflows/test-integration.yml: -------------------------------------------------------------------------------- 1 | name: test-integration 2 | on: [ push ] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | continue-on-error: ${{ matrix.experimental }} 7 | strategy: 8 | matrix: 9 | php-versions: [ '8.2', '8.4'] 10 | experimental: [false] 11 | timeout-minutes: 15 12 | name: PHP ${{ matrix.php-versions }} on Ubuntu latest. Experimental == ${{ matrix.experimental }} 13 | steps: 14 | - name: Install PHP 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: ${{ matrix.php-versions }} 18 | - name: Checkout 19 | uses: actions/checkout@master 20 | - name: Install dependencies 21 | run: composer install 22 | continue-on-error: ${{ matrix.experimental }} 23 | - id: checks 24 | name: Run CI tests 25 | run: composer check-ci 26 | continue-on-error: ${{ matrix.experimental }} 27 | - name: Output log files on failure 28 | if: failure() 29 | run: tail -2000 /var/log/syslog 30 | -------------------------------------------------------------------------------- /src/Exception/UnknownUrnException.php: -------------------------------------------------------------------------------- 1 | attributes; 32 | } 33 | 34 | public function isResolved(): bool 35 | { 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SAML2/Attribute/AttributeSetFactory.php: -------------------------------------------------------------------------------- 1 | signatureAlgorithm 32 | ) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ci/qa/phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is used by the Ibuildings QA tools to wrap the coding standard of your choice. 5 | By default it is less stringent about long lines than other coding standards 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | src/Tests/* 19 | 20 | 21 | 22 | src/Tests/* 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Http/XMLResponse.php: -------------------------------------------------------------------------------- 1 | headers->get('Content-Type') !== 'application/xml') { 34 | $this->headers->set('Content-Type', 'application/xml'); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Security/Authentication/Token/SamlToken.php: -------------------------------------------------------------------------------- 1 | setAttributes($attributes); 31 | } 32 | 33 | public function getCredentials(): string 34 | { 35 | return ''; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Exception/UnknownServiceProviderException.php: -------------------------------------------------------------------------------- 1 | entityId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ci/qa/phpmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | Ibuildings QA Tools Default Ruleset 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | */Tests/* 36 | 37 | -------------------------------------------------------------------------------- /src/Tests/Component/Extensions/written_extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | https://gateway.example.com/gssp/tiqr/metadatatest@test.nlDoe 1https://selfservice.stepup.example.com/registration/gssf/tiqr/metadata 3 | -------------------------------------------------------------------------------- /src/Tests/Component/Metadata/keys/idp-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDuDCCAqCgAwIBAgIJAPdqJ9JQKN6vMA0GCSqGSIb3DQEBBQUAMEYxDzANBgNV 3 | BAMTBkVuZ2luZTERMA8GA1UECxMIU2VydmljZXMxEzARBgNVBAoTCk9wZW5Db25l 4 | eHQxCzAJBgNVBAYTAk5MMB4XDTE1MDQwMjE0MDE1NFoXDTI1MDQwMTE0MDE1NFow 5 | RjEPMA0GA1UEAxMGRW5naW5lMREwDwYDVQQLEwhTZXJ2aWNlczETMBEGA1UEChMK 6 | T3BlbkNvbmV4dDELMAkGA1UEBhMCTkwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 7 | ggEKAoIBAQCeVodghQwFR0pItxGaJ3LXHA+ZLy1w/TMaGDcJaszAZRWRkL/6djwb 8 | abR7TB45QN6dfKOFGzobQxG1Oksky3gz4Pki1BSzi/DwsjWCw+Yi40cYpYeg/XM0 9 | tvHKVorlsx/7Thm5WuC7rwytujr/lV7f6lavf/ApnLHnOORU2h0ZWctJiestapMa 10 | C5mc40msruWWp04axmrYICmTmGhEy7w0qO4/HLKjXtWbJh71GWtJeLzG5Hj04X44 11 | wI+D9PUJs9U3SYh9SCFZwq0v+oYeqajiX0JPzB+8aVOPmOOM5WqoT8OCddOM/Tls 12 | L/0PcxByGHsgJuWbWMI1PKlK3omR764PAgMBAAGjgagwgaUwHQYDVR0OBBYEFLow 13 | msUCD2CrHU0lich1DMkNppmLMHYGA1UdIwRvMG2AFLowmsUCD2CrHU0lich1DMkN 14 | ppmLoUqkSDBGMQ8wDQYDVQQDEwZFbmdpbmUxETAPBgNVBAsTCFNlcnZpY2VzMRMw 15 | EQYDVQQKEwpPcGVuQ29uZXh0MQswCQYDVQQGEwJOTIIJAPdqJ9JQKN6vMAwGA1Ud 16 | EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAIF9tGG1C9HOSTQJA5qL13y5Ad8G 17 | 57bJjBfTjp/dw308zwagsdTeFQIgsP4tdQqPMwYmBImcTx6vUNdiwlIol7TBCPGu 18 | qQAHD0lgTkChCzWezobIPxjitlkTUZGHqn4Kpq+mFelX9x4BElmxdLj0RQV3c3Bh 19 | oW0VvJvBkqVKWkZ0HcUTQMlMrQEOq6D32jGh0LPCQN7Ke6ir0Ix5knb7oegND49f 20 | bLSxpdo5vSuxQd+Zn6nI1/VLWtWpdeHMKhiw2+/ArR9YM3cY8UwFQOj9Y6wI6gPC 21 | Gh/q1qv2HnngmnPrNzZik8XucGcf1Wm2zE4UIVYKW31T52mqRVDKRk8F3Eo= 22 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /src/Tests/Unit/Metadata/invalid_certificate.pem: -------------------------------------------------------------------------------- 1 | -- 2 | MIIDwTCCAqmgAwIBAgIUYuSUugwc4J4NyW9WGqYJ/liwM4owDQYJKoZIhvcNAQEL 3 | BQAwcDELMAkGA1UEBhMCTkwxEDAOBgNVBAgMB1V0cmVjaHQxEDAOBgNVBAcMB1V0 4 | cmVjaHQxJzAlBgNVBAoMHkRldmVsb3BtZW50IERvY2tlciBlbnZpcm9ubWVudDEU 5 | MBIGA1UEAwwLR2F0ZXdheSBJRFAwHhcNMjMwNTE3MTIxNTEyWhcNMzMwNTE0MTIx 6 | NTEyWjBwMQswCQYDVQQGEwJOTDEQMA4GA1UECAwHVXRyZWNodDEQMA4GA1UEBwwH 7 | VXRyZWNodDEnMCUGA1UECgweRGV2ZWxvcG1lbnQgRG9ja2VyIGVudmlyb25tZW50 8 | MRQwEgYDVQQDDAtHYXRld2F5IElEUDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 9 | AQoCggEBAM2ulQVs5WpbJOAf7Cv/VPDTJqbWHVdUxAmdwZJlcNTRKNFVp4aJzQ3d 10 | piyiGghI5odnzU0/BWBoHZFNYPU/OFr/gzn6iJGxL63L9+mFgE8PR9HpkV5TaRnr 11 | 21+nZ0EXWjDZk9Px0enERicCItTeQzAUJeA0A9miIcK5IKIz/zSBSR3c802SGD/V 12 | elUqY7Z2/UJM97cT92L+4Fz+4zhxxoThbPbrR0CweiROIt82grdwg7zf0+b62MOu 13 | VtqFh0yPLRAFfLc4LjHuxFUdUvOHVta7x74dwdmHikqfujM10XN+sNns3LDJde2y 14 | PWchU6ktq7cjgbYfIW/vzVzafP1Jk40CAwEAAaNTMFEwHQYDVR0OBBYEFGYn6LWR 15 | DZa7+YryUncIlwJB2VorMB8GA1UdIwQYMBaAFGYn6LWRDZa7+YryUncIlwJB2Vor 16 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ57lcOF6PWWW56m 17 | S2s5gKFImtfRFzlfiyHsF14L7+nQ5NjfOhpU0wRpnTjK91KP0wCwlxzGFXR8yfqf 18 | BFJryIV7aDdYPH/RIkwVaNBI0fsD/ozlYb18seieDEGLvQtTlrmc0UNHtWz6FW3L 19 | 2geM3ENaqpOATl1Ywp4EPML7Dh0CbhhyM8PnPCEsdclouIeP5/B9Swfk3omXehof 20 | 6bkFbntqA03msFBiW50twkfKeKULcJGXo667hto27KNxZUauqtPbnAGpUQmge8nx 21 | SQlN8RPwlvygVM4LVMF9qP9YxloTH0xVNwN4noZUhfMNsKoJ7Hg5Xulaok8oCqmz 22 | EiSroEg= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /src/SAML2/Attribute/Attribute.php: -------------------------------------------------------------------------------- 1 | attributeDefinition; 35 | } 36 | 37 | public function getValue(): array 38 | { 39 | return $this->value; 40 | } 41 | 42 | public function equals(Attribute $other): bool 43 | { 44 | return $this->attributeDefinition->equals($other->attributeDefinition) && $this->value === $other->value; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Tests/Unit/Metadata/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDwTCCAqmgAwIBAgIUYuSUugwc4J4NyW9WGqYJ/liwM4owDQYJKoZIhvcNAQEL 3 | BQAwcDELMAkGA1UEBhMCTkwxEDAOBgNVBAgMB1V0cmVjaHQxEDAOBgNVBAcMB1V0 4 | cmVjaHQxJzAlBgNVBAoMHkRldmVsb3BtZW50IERvY2tlciBlbnZpcm9ubWVudDEU 5 | MBIGA1UEAwwLR2F0ZXdheSBJRFAwHhcNMjMwNTE3MTIxNTEyWhcNMzMwNTE0MTIx 6 | NTEyWjBwMQswCQYDVQQGEwJOTDEQMA4GA1UECAwHVXRyZWNodDEQMA4GA1UEBwwH 7 | VXRyZWNodDEnMCUGA1UECgweRGV2ZWxvcG1lbnQgRG9ja2VyIGVudmlyb25tZW50 8 | MRQwEgYDVQQDDAtHYXRld2F5IElEUDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 9 | AQoCggEBAM2ulQVs5WpbJOAf7Cv/VPDTJqbWHVdUxAmdwZJlcNTRKNFVp4aJzQ3d 10 | piyiGghI5odnzU0/BWBoHZFNYPU/OFr/gzn6iJGxL63L9+mFgE8PR9HpkV5TaRnr 11 | 21+nZ0EXWjDZk9Px0enERicCItTeQzAUJeA0A9miIcK5IKIz/zSBSR3c802SGD/V 12 | elUqY7Z2/UJM97cT92L+4Fz+4zhxxoThbPbrR0CweiROIt82grdwg7zf0+b62MOu 13 | VtqFh0yPLRAFfLc4LjHuxFUdUvOHVta7x74dwdmHikqfujM10XN+sNns3LDJde2y 14 | PWchU6ktq7cjgbYfIW/vzVzafP1Jk40CAwEAAaNTMFEwHQYDVR0OBBYEFGYn6LWR 15 | DZa7+YryUncIlwJB2VorMB8GA1UdIwQYMBaAFGYn6LWRDZa7+YryUncIlwJB2Vor 16 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ57lcOF6PWWW56m 17 | S2s5gKFImtfRFzlfiyHsF14L7+nQ5NjfOhpU0wRpnTjK91KP0wCwlxzGFXR8yfqf 18 | BFJryIV7aDdYPH/RIkwVaNBI0fsD/ozlYb18seieDEGLvQtTlrmc0UNHtWz6FW3L 19 | 2geM3ENaqpOATl1Ywp4EPML7Dh0CbhhyM8PnPCEsdclouIeP5/B9Swfk3omXehof 20 | 6bkFbntqA03msFBiW50twkfKeKULcJGXo667hto27KNxZUauqtPbnAGpUQmge8nx 21 | SQlN8RPwlvygVM4LVMF9qP9YxloTH0xVNwN4noZUhfMNsKoJ7Hg5Xulaok8oCqmz 22 | EiSroEg= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /src/SurfnetSamlBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new SpRepositoryAliasCompilerPass()); 34 | $container->addCompilerPass(new SamlAttributeRegistrationCompilerPass()); 35 | 36 | $container->registerExtension(new SurfnetSamlExtension()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SAML2/Response/Assertion/InResponseTo.php: -------------------------------------------------------------------------------- 1 | getSubjectConfirmation(); 35 | 36 | if ($subjectConfirmationArray === []) { 37 | return null; 38 | } 39 | 40 | $subjectConfirmation = $subjectConfirmationArray[0]; 41 | return $subjectConfirmation->getSubjectConfirmationData()?->getInResponseTo(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/SAML2/Extensions/Extensions.php: -------------------------------------------------------------------------------- 1 | chunks[$chunk->getName()] = $chunk; 32 | } 33 | 34 | public function getChunks(): array 35 | { 36 | return $this->chunks; 37 | } 38 | 39 | public function getGsspUserAttributesChunk(): ?GsspUserAttributesChunk 40 | { 41 | if (!$this->hasGsspUserAttributesChunk()) { 42 | return null; 43 | } 44 | return new GsspUserAttributesChunk($this->chunks['UserAttributes']->getValue()); 45 | } 46 | 47 | public function hasGsspUserAttributesChunk(): bool 48 | { 49 | return array_key_exists('UserAttributes', $this->chunks); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Resources/keys/development_publickey.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJTCCAw2gAwIBAgIJANug+o++1X5IMA0GCSqGSIb3DQEBCwUAMIGoMQswCQYD 3 | VQQGEwJOTDEQMA4GA1UECAwHVXRyZWNodDEQMA4GA1UEBwwHVXRyZWNodDEVMBMG 4 | A1UECgwMU1VSRm5ldCBCLlYuMRMwEQYDVQQLDApTVVJGY29uZXh0MRwwGgYDVQQD 5 | DBNTVVJGbmV0IERldmVsb3BtZW50MSswKQYJKoZIhvcNAQkBFhxzdXJmY29uZXh0 6 | LWJlaGVlckBzdXJmbmV0Lm5sMB4XDTE0MTAyMDEyMzkxMVoXDTE0MTExOTEyMzkx 7 | MVowgagxCzAJBgNVBAYTAk5MMRAwDgYDVQQIDAdVdHJlY2h0MRAwDgYDVQQHDAdV 8 | dHJlY2h0MRUwEwYDVQQKDAxTVVJGbmV0IEIuVi4xEzARBgNVBAsMClNVUkZjb25l 9 | eHQxHDAaBgNVBAMME1NVUkZuZXQgRGV2ZWxvcG1lbnQxKzApBgkqhkiG9w0BCQEW 10 | HHN1cmZjb25leHQtYmVoZWVyQHN1cmZuZXQubmwwggEiMA0GCSqGSIb3DQEBAQUA 11 | A4IBDwAwggEKAoIBAQDXuSSBeNJY3d4p060oNRSuAER5nLWT6AIVbv3XrXhcgSwc 12 | 9m2b8u3ksp14pi8FbaNHAYW3MjlKgnLlopYIylzKD/6Ut/clEx67aO9Hpqsc0HmI 13 | P0It6q2bf5yUZ71E4CN2HtQceO5DsEYpe5M7D5i64kS2A7e2NYWVdA5Z01DqUpQG 14 | RBc+uMzOwyif6StBiMiLrZH3n2r5q5aVaXU4Vy5EE4VShv3Mp91sgXJj/v155fv0 15 | wShgl681v8yf2u2ZMb7NKnQRA4zM2Ng2EUAyy6PQ+Jbn+rALSm1YgiJdVuSlTLhv 16 | gwbiHGO2XgBi7bTHhlqSrJFK3Gs4zwIsop/XqQRBAgMBAAGjUDBOMB0GA1UdDgQW 17 | BBQCJmcoa/F7aM3jIFN7Bd4uzWRgzjAfBgNVHSMEGDAWgBQCJmcoa/F7aM3jIFN7 18 | Bd4uzWRgzjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBd80GpWKjp 19 | 1J+Dgp0blVAox1s/WPWQlex9xrx1GEYbc5elp3svS+S82s7dFm2llHrrNOBt1HZV 20 | C+TdW4f+MR1xq8O5lOYjDRsosxZc/u9jVsYWYc3M9bQAx8VyJ8VGpcAK+fLqRNab 21 | YlqTnj/t9bzX8fS90sp8JsALV4g84Aj0G8RpYJokw+pJUmOpuxsZN5U84MmLPnVf 22 | mrnuCVh/HkiLNV2c8Pk8LSomg6q1M1dQUTsz/HVxcOhHLj/owwh3IzXf/KXV/E8v 23 | SYW8o4WWCAnruYOWdJMI4Z8NG1Mfv7zvb7U3FL1C/KLV04DqzALXGj+LVmxtDvux 24 | qC042apoIDQV 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /src/SAML2/Extensions/Chunk.php: -------------------------------------------------------------------------------- 1 | name; 36 | } 37 | 38 | public function getNamespace(): string 39 | { 40 | return $this->namespace; 41 | } 42 | 43 | public function append(DOMElement $element): void 44 | { 45 | $doc = new DOMDocument("1.0", "UTF-8"); 46 | $root = $doc->importNode($this->getValue(), true); 47 | $attrib = $doc->importNode($element, true); 48 | $root->appendChild($attrib); 49 | $doc->appendChild($root); 50 | $this->value = $doc->documentElement; 51 | } 52 | 53 | public function getValue(): DOMElement 54 | { 55 | return $this->value; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/SamlAttributeRegistrationCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('surfnet_saml.saml.attribute_dictionary')) { 33 | return; 34 | } 35 | 36 | $collection = $container->getDefinition('surfnet_saml.saml.attribute_dictionary'); 37 | $attributes = $container->findTaggedServiceIds('saml.attribute'); 38 | 39 | foreach (array_keys($attributes) as $id) { 40 | $collection->addMethodCall('addAttributeDefinition', [new Reference($id)]); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/SAML2/Attribute/AttributeSetInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if metadata.hasIdPMetadata %} 4 | 5 | 6 | 7 | 8 | {{ metadata.idpCertificate }} 9 | 10 | 11 | 12 | 14 | 15 | {% endif %} 16 | {% if metadata.hasSpMetadata %} 17 | 18 | {% if metadata.spCertificate %} 19 | 20 | 21 | 22 | {{ metadata.spCertificate }} 23 | 24 | 25 | 26 | {% endif %} 27 | 30 | 31 | {% endif %} 32 | 33 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/SpRepositoryAliasCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasParameter('surfnet_saml.configuration.service_provider_repository.alias')) { 30 | return; 31 | } 32 | 33 | $alias = $container->getParameter('surfnet_saml.configuration.service_provider_repository.alias'); 34 | 35 | if (!$container->hasDefinition($alias)) { 36 | throw new InvalidConfigurationException(sprintf( 37 | 'The container does not contain the configured entity repository service "%s"', 38 | $alias 39 | )); 40 | } 41 | 42 | $container->setAlias('surfnet_saml.entity.entity_repository', $alias); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Entity/StaticServiceProviderRepository.php: -------------------------------------------------------------------------------- 1 | serviceProviders = new ServiceProviders($serviceProviders); 35 | } 36 | 37 | /** 38 | * @throws NotFound 39 | */ 40 | public function getServiceProvider($entityId): ServiceProvider 41 | { 42 | $serviceProvider = $this->serviceProviders->findByEntityId($entityId); 43 | if ($serviceProvider) { 44 | return $serviceProvider; 45 | } 46 | throw NotFound::identityProvider($entityId); 47 | } 48 | 49 | public function hasServiceProvider(string $entityId): bool 50 | { 51 | return $this->serviceProviders->hasByEntityId($entityId); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tests/Unit/Security/FakeAuthencationStateHandler.php: -------------------------------------------------------------------------------- 1 | requestId = $requestId; 31 | return $stateHandler; 32 | } 33 | 34 | public static function createWithoutRequestId(): self 35 | { 36 | return new self; 37 | } 38 | 39 | public function getRequestId(): string 40 | { 41 | return $this->requestId; 42 | } 43 | 44 | public function setRequestId(string $requestId): void 45 | { 46 | $this->requestId = $requestId; 47 | } 48 | 49 | public function hasRequestId(): bool 50 | { 51 | return $this->requestId !== ''; 52 | } 53 | 54 | public function clearRequestId(): void 55 | { 56 | $this->requestId = ''; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Entity/StaticIdentityProviderRepository.php: -------------------------------------------------------------------------------- 1 | identityProviders = new IdentityProviders($identityProviders); 35 | } 36 | 37 | /** 38 | * @throws NotFound 39 | */ 40 | public function getIdentityProvider(string $entityId): IdentityProvider 41 | { 42 | $identityProvider = $this->identityProviders->findByEntityId($entityId); 43 | if ($identityProvider) { 44 | return $identityProvider; 45 | } 46 | 47 | throw NotFound::identityProvider($entityId); 48 | } 49 | 50 | public function hasIdentityProvider(string $entityId): bool 51 | { 52 | return $this->identityProviders->hasByEntityId($entityId); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Tests/Component/Extensions/with_extensions.xml: -------------------------------------------------------------------------------- 1 | 7 | https://gateway.example.com/gssp/tiqr/metadata 8 | 9 | 12 | 15 | user@example.com 16 | 17 | 19 | foobar 20 | 21 | 22 | 23 | 24 | https://selfservice.stepup.example.com/registration/gssf/tiqr/metadata 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Tests/Component/Metadata/XsdValidator.php: -------------------------------------------------------------------------------- 1 | schemaValidate($xsdPath); 33 | 34 | if (!$isValid) { 35 | $errors = libxml_get_errors(); 36 | $errorMessages = array_map(function ($error) { 37 | return sprintf( 38 | "Line %d: %s", 39 | $error->line, 40 | trim($error->message) 41 | ); 42 | }, $errors); 43 | libxml_clear_errors(); 44 | 45 | return $errorMessages; 46 | } 47 | 48 | libxml_clear_errors(); 49 | return []; 50 | } 51 | 52 | public function isValid(DOMDocument $document, string $xsdPath): bool 53 | { 54 | return empty($this->validate($document, $xsdPath)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Entity/ImmutableCollection/ServiceProviders.php: -------------------------------------------------------------------------------- 1 | serviceProviders = array_values($serviceProviders); 39 | } 40 | 41 | public function hasByEntityId($entityId): bool 42 | { 43 | return $this->findByEntityId($entityId) !== null; 44 | } 45 | 46 | public function findByEntityId($entityId) 47 | { 48 | return $this->find(fn(ServiceProvider $provider): bool => $provider->getEntityId() === $entityId); 49 | } 50 | 51 | private function find(callable $callback) 52 | { 53 | foreach ($this->serviceProviders as $provider) { 54 | if ($callback($provider)) { 55 | return $provider; 56 | } 57 | } 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Entity/ImmutableCollection/IdentityProviders.php: -------------------------------------------------------------------------------- 1 | identityProviders = array_values($identityProviders); 39 | } 40 | 41 | public function hasByEntityId($entityId): bool 42 | { 43 | return $this->findByEntityId($entityId) !== null; 44 | } 45 | 46 | public function findByEntityId($entityId) 47 | { 48 | return $this->find(fn(IdentityProvider $provider): bool => $provider->getEntityId() === $entityId); 49 | } 50 | 51 | private function find(callable $callback) 52 | { 53 | foreach ($this->identityProviders as $provider) { 54 | if ($callback($provider)) { 55 | return $provider; 56 | } 57 | } 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Tests/Component/Extensions/ChunkSerializationTest.php: -------------------------------------------------------------------------------- 1 | addAttribute( 31 | 'urn:mace:dir:attribute-def:mail', 32 | 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 33 | 'test@test.nl' 34 | ); 35 | $chunk->addAttribute( 36 | 'urn:mace:dir:attribute-def:surname', 37 | 'urn:oasis:names:tc:SAML:2.0:attrname-format:string', 38 | 'Doe 1' 39 | ); 40 | 41 | $xmlString = $chunk->toXML(); 42 | 43 | $newChunk = GsspUserAttributesChunk::fromXML($xmlString); 44 | 45 | self::assertEquals($chunk->getName(), $newChunk->getName()); 46 | self::assertEquals($chunk->getNamespace(), $newChunk->getNamespace()); 47 | self::assertEquals($chunk->getAttributeValue('urn:mace:dir:attribute-def:mail'), $newChunk->getAttributeValue('urn:mace:dir:attribute-def:mail')); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Security/Authentication/Handler/FailureHandler.php: -------------------------------------------------------------------------------- 1 | getMessageKey(), 33 | $exception->getMessage() 34 | ); 35 | $this->logger->notice($message); 36 | 37 | $responseBody = " 38 | 39 | 40 | 41 | 42 | Authentication failed 43 | 44 | 45 | $message 46 | 47 | "; 48 | 49 | return new Response($responseBody, Response::HTTP_UNAUTHORIZED); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tests/Unit/Monolog/SamlAuthenticationLoggerTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('emergency')->with('message2', ['sari' => $requestId])->once(); 39 | 40 | $logger = new SamlAuthenticationLogger($innerLogger); 41 | $logger = $logger->forAuthentication($requestId); 42 | $logger->emergency('message2'); 43 | } 44 | 45 | #[Test] 46 | public function it_does_not_throw_when_no_authentication(): void 47 | { 48 | $innerLogger = m::mock(LoggerInterface::class); 49 | $innerLogger->shouldReceive('emergency')->with('message2', [])->once(); 50 | 51 | $logger = new SamlAuthenticationLogger($innerLogger); 52 | $logger->emergency('message2'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/EventSubscriber/BridgeContainerBootListener.php: -------------------------------------------------------------------------------- 1 | ['onKernelRequest', 256], 48 | ]; 49 | } 50 | 51 | public function onKernelRequest(RequestEvent $event): void 52 | { 53 | ContainerSingleton::setContainer($this->bridgeContainer); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Tests/Component/Metadata/keys/entity.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /src/Http/HttpBindingFactory.php: -------------------------------------------------------------------------------- 1 | getMethod()) { 39 | case Request::METHOD_POST: 40 | if ($request->request->has('SAMLRequest')) { 41 | return $this->postBinding; 42 | } else { 43 | throw new InvalidArgumentException('POST-binding is supported for SAMLRequest.'); 44 | } 45 | case Request::METHOD_GET: 46 | if ($request->query->has('SAMLRequest') || $request->query->has('SAMLResponse')) { 47 | return $this->redirectBinding; 48 | } else { 49 | throw new InvalidArgumentException('Redirect binding is supported for SAMLRequest and Response.'); 50 | } 51 | default: 52 | throw new InvalidArgumentException( 53 | sprintf('Request type of "%s" is not supported.', $request->getMethod()) 54 | ); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/SAML2/Attribute/ConfigurableAttributeSetFactory.php: -------------------------------------------------------------------------------- 1 | document) { 70 | throw new LogicException('Cannot get the rootElement of Metadata before the document has been generated'); 71 | } 72 | 73 | return $this->document->documentElement; 74 | } 75 | 76 | public function getAppendBeforeNode(): ?DOMNode 77 | { 78 | if (!$this->document) { 79 | throw new LogicException( 80 | 'Cannot get the appendBeforeNode of Metadata before the document has been generated' 81 | ); 82 | } 83 | 84 | return $this->document->documentElement->childNodes->item(0); 85 | } 86 | 87 | public function __toString(): string 88 | { 89 | return (string) $this->document->saveXML(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Security/Authentication/AuthenticatedSessionStateHandler.php: -------------------------------------------------------------------------------- 1 | name = $name; 47 | $this->urnMace = $urnMace; 48 | $this->urnOid = $urnOid; 49 | } 50 | 51 | public function getName(): string 52 | { 53 | return $this->name; 54 | } 55 | 56 | public function hasUrnMace(): bool 57 | { 58 | return $this->urnMace !== null; 59 | } 60 | 61 | public function getUrnMace(): ?string 62 | { 63 | return $this->urnMace; 64 | } 65 | 66 | public function hasUrnOid(): bool 67 | { 68 | return $this->urnOid !== null; 69 | } 70 | 71 | public function getUrnOid(): ?string 72 | { 73 | return $this->urnOid; 74 | } 75 | 76 | public function equals(AttributeDefinition $other): bool 77 | { 78 | return $this->name === $other->name 79 | && $this->urnOid === $other->urnOid 80 | && $this->urnMace === $other->urnMace; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/SAML2/Extensions/ExtensionsMapperTrait.php: -------------------------------------------------------------------------------- 1 | extensions = new Extensions(); 30 | if (!empty($this->request->getExtensions())) { 31 | $rawExtensions = $this->request->getExtensions(); 32 | /** @var SAML2Chunk $rawChunk */ 33 | foreach ($rawExtensions as $rawChunk) { 34 | match ($rawChunk->getLocalName()) { 35 | 'UserAttributes' => $this->extensions->addChunk( 36 | new GsspUserAttributesChunk($rawChunk->getXML()) 37 | ), 38 | default => $this->extensions->addChunk( 39 | new Chunk( 40 | $rawChunk->getLocalName(), 41 | $rawChunk->getNamespaceURI(), 42 | $rawChunk->getXML() 43 | ) 44 | ), 45 | }; 46 | } 47 | } 48 | } 49 | 50 | public function getExtensions(): Extensions 51 | { 52 | if (!isset($this->extensions)) { 53 | $this->extensions = new Extensions(); 54 | } 55 | return $this->extensions; 56 | } 57 | 58 | public function setExtensions(Extensions $extensions): void 59 | { 60 | $this->extensions = $extensions; 61 | $samlExt = []; 62 | foreach ($this->extensions->getChunks() as $chunk) { 63 | $samlExt[] = new SAML2Chunk($chunk->getValue()); 64 | } 65 | $this->request->setExtensions($samlExt); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Tests/Unit/Metadata/MetadataFactoryTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(MetadataFactory::class) 37 | ->disableOriginalConstructor() 38 | ->onlyMethods(['getCertificateData']) 39 | ->getMock(); 40 | 41 | // Setup a reflection to call the private method 42 | $reflectionMethod = new ReflectionMethod($metadataFactoryMock::class, 'getCertificateData'); 43 | 44 | // Test getCertificateData method with a valid certificate 45 | $result = $reflectionMethod->invoke($metadataFactoryMock, $publicKeyFile); 46 | $this->assertEquals($expectedCertificate, $result); 47 | 48 | // Test with an invalid certificate 49 | $invalidPublicKeyFile = __DIR__ . '/invalid_certificate.pem'; // File with invalid certificate 50 | $this->expectException(RuntimeException::class); 51 | $this->expectExceptionMessage('Could not parse PEM certificate in ' . $invalidPublicKeyFile); 52 | $reflectionMethod->invoke($metadataFactoryMock, $invalidPublicKeyFile); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Resources/config/services_authentication.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # By default, we reject no SAML responses, but you are able to do this 3 | # by configuring a certain relay state value that drops support for the 4 | # SamlAuthenticator::supports method call. Usefull when you want to 5 | # a SAML response on a custom ACS location. 6 | rejected_relay_states: [] 7 | 8 | services: 9 | _defaults: 10 | public: false 11 | autowire: true 12 | autoconfigure: true 13 | 14 | Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger: 15 | alias: surfnet_saml.logger 16 | 17 | Surfnet\SamlBundle\Security\Authentication\Handler\SuccessHandler: 18 | arguments: 19 | - '@security.http_utils' 20 | - [] 21 | - '@logger' 22 | 23 | Surfnet\SamlBundle\Security\Authentication\Handler\FailureHandler: 24 | arguments: 25 | - '@kernel' 26 | - '@security.http_utils' 27 | - [ ] 28 | - '@logger' 29 | 30 | Surfnet\SamlBundle\Security\Authentication\Session\SessionStorage: 31 | arguments: 32 | - '@request_stack' 33 | 34 | Surfnet\SamlBundle\Security\Authentication\Handler\ProcessSamlAuthenticationHandler: 35 | arguments: 36 | - '@Surfnet\SamlBundle\Security\Authentication\SamlInteractionProvider' 37 | - '@Surfnet\SamlBundle\Security\Authentication\Session\SessionStorage' 38 | - '@surfnet_saml.logger' 39 | 40 | Surfnet\SamlBundle\Security\Authentication\SamlInteractionProvider: 41 | arguments: 42 | - '@surfnet_saml.hosted.service_provider' 43 | - '@surfnet_saml.remote.idp' 44 | - '@surfnet_saml.http.redirect_binding' 45 | - '@surfnet_saml.http.post_binding' 46 | - '@Surfnet\SamlBundle\Security\Authentication\Session\SessionStorage' 47 | 48 | Surfnet\SamlBundle\Security\Authentication\SamlAuthenticator: 49 | arguments: 50 | - '@surfnet_saml.remote.idp' 51 | - '@surfnet_saml.hosted.service_provider' 52 | - '@surfnet_saml.http.redirect_binding' 53 | - '@Surfnet\SamlBundle\Security\Authentication\Session\SessionStorage' 54 | - '@Surfnet\SamlBundle\Security\Authentication\Handler\ProcessSamlAuthenticationHandler' 55 | - '@Surfnet\SamlBundle\Security\Authentication\Handler\SuccessHandler' 56 | - '@Surfnet\SamlBundle\Security\Authentication\Handler\FailureHandler' 57 | - '@surfnet_saml.saml_provider' 58 | - '@router' 59 | - '@logger' 60 | - '%acs_location_route_name%' 61 | - '%rejected_relay_states%' 62 | - '%authentication_context_class_ref%' 63 | -------------------------------------------------------------------------------- /src/Tests/TestSaml2Container.php: -------------------------------------------------------------------------------- 1 | logger; 34 | } 35 | 36 | /** 37 | * Generate a random identifier for identifying SAML2 documents. 38 | */ 39 | public function generateId() : string 40 | { 41 | return '1'; 42 | } 43 | 44 | public function debugMessage($message, string $type) : void 45 | { 46 | $this->logger->debug($message, ['type' => $type]); 47 | } 48 | 49 | public function redirect(string $url, array $data = []) : void 50 | { 51 | throw new BadMethodCallException( 52 | sprintf( 53 | "[TEST] %s:%s may not be called in the Surfnet\\SamlBundle as it doesn't work with Symfony2", 54 | self::class, 55 | __METHOD__ 56 | ) 57 | ); 58 | } 59 | 60 | public function postRedirect(string $url, array $data = []) : void 61 | { 62 | throw new BadMethodCallException( 63 | sprintf( 64 | "[TEST] %s:%s may not be called in the Surfnet\\SamlBundle as it doesn't work with Symfony2", 65 | self::class, 66 | __METHOD__ 67 | ) 68 | ); 69 | } 70 | 71 | public function getTempDir(): string 72 | { 73 | // TODO: Implement getTempDir() method. 74 | } 75 | 76 | public function writeFile(string $filename, string $data, int $mode = null): void 77 | { 78 | // TODO: Implement writeFile() method. 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Attribute/Mock/DummyAttributeSet.php: -------------------------------------------------------------------------------- 1 | attributeSet = ConfigurableAttributeSetFactory::createFrom($assertion, $attributeDictionary); 36 | } 37 | 38 | public function getNameID(): ?string 39 | { 40 | $data = $this->assertion->getNameId(); 41 | if ($data instanceof NameID) { 42 | return $data->getValue(); 43 | } 44 | 45 | return null; 46 | } 47 | 48 | /** 49 | * @param string $attributeName the name of the attribute to attempt to get the value of 50 | * @param mixed $defaultValue the value to return should the assertion not contain the attribute 51 | * @return string[]|mixed string[] if the attribute is found, the given default value otherwise 52 | */ 53 | public function getAttributeValue($attributeName, mixed $defaultValue = null): mixed 54 | { 55 | $attributeDefinition = $this->attributeDictionary->getAttributeDefinition($attributeName); 56 | 57 | if (!$this->attributeSet->containsAttributeDefinedBy($attributeDefinition)) { 58 | return $defaultValue; 59 | } 60 | 61 | $attribute = $this->attributeSet->getAttributeByDefinition($attributeDefinition); 62 | 63 | return $attribute->getValue(); 64 | } 65 | 66 | public function getAttributeSet(): AttributeSetInterface 67 | { 68 | return $this->attributeSet; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Resources/valid-signed-adfs.xml: -------------------------------------------------------------------------------- 1 | 2 | https://gateway.pilot.stepup.surfconext.nl/second-factor-only/metadata 3 | ILS3cdS9l/7XKfxZskH74RPk/cBfhIV0yzPKsYdR2xc=RS088/J+cKscYJuVwPgEaLfdEd1EfZjo7b7qMJqrhVFnb8KDTjHN0YedtBX8nWO2pW3syWv43Fv1fa80zrhAPLAzjsxGvi0Ve9PPTuIoTCeJvm4qFYRj6NUXw1EF+pT44KaVphpA41wuVZUOn97jisBsSAwCLeNkXwojRmbLwmkKiCLqZkaqXhgkfKSUVo1n5r9ksjFQ5anir2K04+MY7daFd8ngHVFNPtKoFTSdNaLP8STp2G7uMr3EFFBdspK/j5kGebALcZMK2YtgZiaVR1hCT1cS1qNnMhpGxcouokw4YuheQE6IOjm18aJLDzivItnNI3RNA9ZjHXidXEqWJg==MIIDPDCCAiSgAwIBAgIQS9XgPuQJ3Z9Eusyz87i/KDANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDDBVzaWduaW5nLmV5bGVtYW5zY2gubmwwHhcNMTcwMzA3MDkxOTEwWhcNMjAxMjMwMjMwMDAwWjAgMR4wHAYDVQQDDBVzaWduaW5nLmV5bGVtYW5zY2gubmwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCW35tGrUhaV1jed2WSXCJ6uHGybxpdTGaEctU1VgOYOw4r9fagldDK8Dkfz/9wnK+zE/E6C8agKzKkKxCdLTYahh8Oj1kyKylY5RU3XZCxOuiRABtLo0VILZEmRa1snrA689QbS1IKWd5QrpCP8SygwmkO90qbUOMilAf4/NzpgNTMVVXrde3+VfhHv9QjXdFzAWkSyXGEOVPDl6ZObBD7F5NjK0jC8lGxsqOtYiMmHPXMVPyPcP2OjDFApYkw4XpWNqfDsex2uGbuDKA96jpqkjDFwQakPnf9CZP+mr43zeWgd9FcxzikRN65mKq8521zFoV70ml+eaLOs7T71KQtAgMBAAGjcjBwMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwIAYDVR0RBBkwF4IVc2lnbmluZy5leWxlbWFuc2NoLm5sMB0GA1UdDgQWBBT71q214wlOLP2QSPRNPCALxmR/nDANBgkqhkiG9w0BAQsFAAOCAQEAHp4xMFCAnK4s1QQpF2fF1BEBoBJegRbvGSukCAyKBE2Q1dPnB2HuLm8GiGKCCVuAsPlO0bBvqe+9hsfKsfVz/k+aLzRl2XKB+CF4uz4npAr0VC807bsbJLdqxdlbkid7VAAFvRABdHldfCMn8HM+1N2LpSkNJDuYBVJqnGfsdAP9H97F36DN1BOyLg48TxjzQEPatx7vYY0a2MvWIulGVVKMDIvEeAboJ6FrG4/A8apQbQ3tq53JK0m+Cp8xoPEM3KKOKzFCgfyrHjJtOGGNPihpq1jXEW1xOJdkw3bcM9AB0ARRZZxO2qmSa6o5HOejkHAxKo0k/76/WLVeaMdvtw== 4 | 5 | urn:collab:person::homeorganization.nl:useridentifier 6 | 7 | -------------------------------------------------------------------------------- /src/Http/ReceivedAuthnRequestPost.php: -------------------------------------------------------------------------------- 1 | samlRequest = $samlRequest; 40 | } 41 | 42 | public static function parse(array $parameters): self 43 | { 44 | if (base64_decode((string) $parameters[self::PARAMETER_REQUEST], true) === false) { 45 | throw new InvalidRequestException('Failed decoding SAML request, did not receive a valid base64 string'); 46 | } 47 | 48 | $parsed = new self($parameters[self::PARAMETER_REQUEST]); 49 | 50 | if (isset($parameters[self::PARAMETER_RELAY_STATE])) { 51 | $parsed->relayState = $parameters[self::PARAMETER_RELAY_STATE]; 52 | } 53 | 54 | $decoded = $parsed->getDecodedSamlRequest(); 55 | $request = ReceivedAuthnRequest::from($decoded); 56 | 57 | $parsed->receivedRequest = $request; 58 | 59 | // Return AuthnRequest 60 | return $parsed; 61 | } 62 | 63 | public function hasRelayState(): bool 64 | { 65 | return $this->relayState !== null; 66 | } 67 | 68 | public function getDecodedSamlRequest(): string 69 | { 70 | return base64_decode($this->samlRequest); 71 | } 72 | 73 | public function getSamlRequest(): string 74 | { 75 | return $this->samlRequest; 76 | } 77 | 78 | public function getRelayState(): ?string 79 | { 80 | return $this->relayState; 81 | } 82 | 83 | /** 84 | * @throws Exception when signature is invalid (@see SAML2\Utils::validateSignature) 85 | */ 86 | public function verify(XMLSecurityKey $key): bool 87 | { 88 | return $this->receivedRequest->verify($key); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Service/SigningService.php: -------------------------------------------------------------------------------- 1 | loadPublicKeyFromFile($keyPair->publicKeyFile); 42 | $privateKey = $this->loadPrivateKeyFromFile($keyPair->privateKeyFile); 43 | 44 | $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']); 45 | $key->loadKey($privateKey->getKeyAsString()); 46 | 47 | Utils::insertSignature( 48 | $key, 49 | [$publicKey->getCertificate()], 50 | $signable->getRootDomElement(), 51 | $signable->getAppendBeforeNode() 52 | ); 53 | 54 | return $signable; 55 | } 56 | 57 | /** 58 | * @param string $publicKeyFile /full/path/to/the/public/key 59 | */ 60 | public function loadPublicKeyFromFile(string $publicKeyFile): X509 61 | { 62 | $this->publicKeyLoader->loadCertificateFile($publicKeyFile); 63 | $keyCollection = $this->publicKeyLoader->getKeys(); 64 | $publicKey = $keyCollection->getOnlyElement(); 65 | 66 | // remove it from the collection so we can reuse the publicKeyLoader for consecutive signing 67 | $keyCollection->remove($publicKey); 68 | 69 | return $publicKey; 70 | } 71 | 72 | /** 73 | * @param string $privateKeyFile /full/path/to/the/private/key 74 | * @return PrivateKey 75 | */ 76 | public function loadPrivateKeyFromFile(string $privateKeyFile): PrivateKey 77 | { 78 | $privateKey = new PrivateKeyFile($privateKeyFile, 'metadata'); 79 | return $this->privateKeyLoader->loadPrivateKey($privateKey); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/SAML2/BridgeContainer.php: -------------------------------------------------------------------------------- 1 | logger; 43 | } 44 | 45 | /** 46 | * Generate a random identifier for identifying SAML2 documents. 47 | */ 48 | public function generateId(): string 49 | { 50 | return '_' . bin2hex(openssl_random_pseudo_bytes(30)); 51 | } 52 | 53 | public function debugMessage($message, $type): void 54 | { 55 | if ($message instanceof DOMElement) { 56 | $message = $message->ownerDocument->saveXML($message); 57 | } 58 | 59 | if (!is_string($message)) { 60 | throw new InvalidArgumentException("Debug message error: could not convert message to string."); 61 | } 62 | 63 | $this->logger->debug($message, ['type' => $type]); 64 | } 65 | 66 | public function redirect($url, $data = []): void 67 | { 68 | $this->notSupported(__METHOD__); 69 | } 70 | 71 | public function postRedirect($url, $data = []): void 72 | { 73 | $this->notSupported(__METHOD__); 74 | } 75 | 76 | /** @throws BadMethodCallException */ 77 | public function getTempDir(): string 78 | { 79 | $this->notSupported(__METHOD__); 80 | return ''; 81 | } 82 | 83 | public function writeFile(string $filename, string $data, ?int $mode = null): void 84 | { 85 | $this->notSupported(__METHOD__); 86 | } 87 | 88 | public function notSupported(string $method): void 89 | { 90 | throw new BadMethodCallException(sprintf( 91 | "%s:%s may not be called in the Surfnet\\SamlBundle", 92 | self::class, 93 | $method 94 | )); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Resources/invalid-missing-signature-value.xml: -------------------------------------------------------------------------------- 1 | http://localhost:8989/simplesaml/module.php/saml/sp/metadata.php/sfo-sp 2 | 3 | 4 | 3j6hSACdbzubiAWM12fpzTuRWgUWgSwxZseXDhQzGXw= 5 | MIIDmTCCAoGgAwIBAgIJAKyUXzwGwcqhMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAk5MMRAwDgYDVQQKDAdFeGFtcGxlMR4wHAYDVQQDDBVTRk8gRGVtbyBTQU1MIHNpZ25pbmcxIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRAZXhhbXBsZS5vcmcwHhcNMTYxMTA2MDgxOTIzWhcNMjYxMTA0MDgxOTIzWjBjMQswCQYDVQQGEwJOTDEQMA4GA1UECgwHRXhhbXBsZTEeMBwGA1UEAwwVU0ZPIERlbW8gU0FNTCBzaWduaW5nMSIwIAYJKoZIhvcNAQkBFhNzdXBwb3J0QGV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA03h5x2cV6+JKLYHO2BxHhiYoRQi/vMQHW7EHRTyfAptf+1AwuDT83LF4OA82oXw1PTO9ffkb9beFHMxBkHQ7fI7Qq4jjhw9ljtB7BPdN9S+uOhNPAhFHb0hHAIngCGg82PEi9hD18lPfS8OJIK+cSOgrCp2H5N2vel1yRXm4laCc8/nssoIoAkV6wnATBE3oSyDMKpK+evUz/oltryf7iLvfnB8XdP3dDMERaOFqstKrj50SCpMpA6AsKZ674aIHuvO/dUD0v5+UVnDjGl2Pbfz0vp+KhV8sWSQ6oBE44yxpYQBiHJi+1Wq0Vi4Vf+hZjiH4fI+qp2BmV0HAOD0mbwIDAQABo1AwTjAdBgNVHQ4EFgQU2em7W0TJzKoNNV3LNoVHeJaJpG0wHwYDVR0jBBgwFoAU2em7W0TJzKoNNV3LNoVHeJaJpG0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALBmM0fMx8fnNabWIIHsElk6qVGpJ6+4583pYoNT/nXrf/Lx2jwYhyyHTdFONMoHbobY0e28t4sao8GqprGFynHs5ssjhOWpADAYHV2l0lcAt0YISmRbSJk7SfHGNYr4JHI+wgt4Cfwlw6BUsVdiBM0gxFPPQrLMoPmY4ZgQoV3YyJKvq6AhhxGvyl5b54wfaEDmGuANfDSz4c3xAX8KxIOTNevUToyMY3Z2uwwEqHSyp0ayjsMoPsZymKUoNzwHQrWyGd2glqukHEPZuP0ZeHLL6dc6/zVhHt+Pwbrvq2Q1aOfiWfLljYZZZ5PNxMEXsh2ZHTkvw2IA/pYVd59Gabg==urn:collab:person:example.org:studenthttp://pilot.surfconext.nl/assurance/sfo-level2 -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Resources/invalid-empty-signature-value.xml: -------------------------------------------------------------------------------- 1 | http://localhost:8989/simplesaml/module.php/saml/sp/metadata.php/sfo-sp 2 | 3 | 4 | 3j6hSACdbzubiAWM12fpzTuRWgUWgSwxZseXDhQzGXw= 5 | MIIDmTCCAoGgAwIBAgIJAKyUXzwGwcqhMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAk5MMRAwDgYDVQQKDAdFeGFtcGxlMR4wHAYDVQQDDBVTRk8gRGVtbyBTQU1MIHNpZ25pbmcxIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRAZXhhbXBsZS5vcmcwHhcNMTYxMTA2MDgxOTIzWhcNMjYxMTA0MDgxOTIzWjBjMQswCQYDVQQGEwJOTDEQMA4GA1UECgwHRXhhbXBsZTEeMBwGA1UEAwwVU0ZPIERlbW8gU0FNTCBzaWduaW5nMSIwIAYJKoZIhvcNAQkBFhNzdXBwb3J0QGV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA03h5x2cV6+JKLYHO2BxHhiYoRQi/vMQHW7EHRTyfAptf+1AwuDT83LF4OA82oXw1PTO9ffkb9beFHMxBkHQ7fI7Qq4jjhw9ljtB7BPdN9S+uOhNPAhFHb0hHAIngCGg82PEi9hD18lPfS8OJIK+cSOgrCp2H5N2vel1yRXm4laCc8/nssoIoAkV6wnATBE3oSyDMKpK+evUz/oltryf7iLvfnB8XdP3dDMERaOFqstKrj50SCpMpA6AsKZ674aIHuvO/dUD0v5+UVnDjGl2Pbfz0vp+KhV8sWSQ6oBE44yxpYQBiHJi+1Wq0Vi4Vf+hZjiH4fI+qp2BmV0HAOD0mbwIDAQABo1AwTjAdBgNVHQ4EFgQU2em7W0TJzKoNNV3LNoVHeJaJpG0wHwYDVR0jBBgwFoAU2em7W0TJzKoNNV3LNoVHeJaJpG0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALBmM0fMx8fnNabWIIHsElk6qVGpJ6+4583pYoNT/nXrf/Lx2jwYhyyHTdFONMoHbobY0e28t4sao8GqprGFynHs5ssjhOWpADAYHV2l0lcAt0YISmRbSJk7SfHGNYr4JHI+wgt4Cfwlw6BUsVdiBM0gxFPPQrLMoPmY4ZgQoV3YyJKvq6AhhxGvyl5b54wfaEDmGuANfDSz4c3xAX8KxIOTNevUToyMY3Z2uwwEqHSyp0ayjsMoPsZymKUoNzwHQrWyGd2glqukHEPZuP0ZeHLL6dc6/zVhHt+Pwbrvq2Q1aOfiWfLljYZZZ5PNxMEXsh2ZHTkvw2IA/pYVd59Gabg==urn:collab:person:example.org:studenthttp://pilot.surfconext.nl/assurance/sfo-level2 -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surfnet/stepup-saml-bundle", 3 | "type": "symfony-bundle", 4 | "description": "A Symfony 7 bundle that integrates the simplesamlphp\\saml2 library with Symfony.", 5 | "keywords": ["surfnet", "StepUp", "simplesamlphp", "SAML", "SAML2"], 6 | "license": "Apache-2.0", 7 | "minimum-stability": "stable", 8 | "require": { 9 | "php": "^8.1", 10 | "ext-dom": "*", 11 | "ext-openssl": "*", 12 | "psr/log": "^3.0", 13 | "robrichards/xmlseclibs": "^3.1.4", 14 | "simplesamlphp/saml2": "^4.6", 15 | "symfony/dependency-injection": "^6.3|^7.0", 16 | "symfony/framework-bundle": "^6.3|^7.0", 17 | "symfony/security-bundle": "^6.3|^7.0", 18 | "symfony/templating": "^6.3|7.0", 19 | "twig/twig": "^3" 20 | }, 21 | "conflict": { 22 | "symfony/http-foundation": ">=8.0" 23 | }, 24 | "require-dev": { 25 | "ext-libxml": "*", 26 | "ext-zlib": "*", 27 | "irstea/phpcpd-shim": "^6.0", 28 | "malukenho/docheader": "^1.1", 29 | "mockery/mockery": "^1.5", 30 | "overtrue/phplint": "*", 31 | "phpmd/phpmd": "^2.6", 32 | "phpstan/extension-installer": "^1.4", 33 | "phpstan/phpstan": "^2.1", 34 | "phpunit/phpunit": "^11.0.0", 35 | "rector/rector": "^2.2", 36 | "sebastian/exporter": "^6.3", 37 | "slevomat/coding-standard": "^8.24", 38 | "squizlabs/php_codesniffer": "^4.0", 39 | "symfony/phpunit-bridge": "^7.3.4" 40 | }, 41 | "scripts": { 42 | "check": [ 43 | "@check-ci", 44 | "@rector" 45 | ], 46 | "check-ci": [ 47 | "@composer-validate", 48 | "@license-headers", 49 | "@phplint", 50 | "@phpcpd", 51 | "@phpcs", 52 | "@phpmd", 53 | "@test", 54 | "@phpstan", 55 | "@composer audit" 56 | ], 57 | "composer-validate": "./ci/qa/validate", 58 | "phplint": "./ci/qa/phplint", 59 | "phpcs": "./ci/qa/phpcs", 60 | "phpcpd": "./ci/qa/phpcpd", 61 | "phpmd": "./ci/qa/phpmd", 62 | "phpstan": "./ci/qa/phpstan", 63 | "phpstan-baseline": "./ci/qa/phpstan-update-baseline", 64 | "test": "./ci/qa/phpunit", 65 | "license-headers": "./ci/qa/docheader", 66 | "phpcbf": "./ci/qa/phpcbf", 67 | "rector": "./ci/qa/rector.sh --dry-run", 68 | "rector-fix": "./ci/qa/rector.sh" 69 | }, 70 | "autoload": { 71 | "psr-4": { 72 | "Surfnet\\SamlBundle\\": "src" 73 | } 74 | }, 75 | "extra": { 76 | "phpstan": { 77 | "includes": [ 78 | "./ci/qa/extension.neon" 79 | ] 80 | } 81 | }, 82 | "config": { 83 | "sort-packages": true, 84 | "allow-plugins": { 85 | "dealerdirect/phpcodesniffer-composer-installer": true, 86 | "phpstan/extension-installer": true, 87 | "simplesamlphp/composer-xmlprovider-installer": true 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/ReceivedAuthnRequestPostTest.php: -------------------------------------------------------------------------------- 1 | base64_encode($samlRequest), 34 | 'RelayState' => '/index.php', 35 | ]; 36 | $authnRequest = ReceivedAuthnRequestPost::parse($parameters); 37 | $this->assertEquals('/index.php', $authnRequest->getRelayState()); 38 | } 39 | 40 | #[Test] 41 | public function it_can_decode_a_signed_saml_request_from_adfs_origin(): void 42 | { 43 | $samlRequest = str_replace(PHP_EOL, '', file_get_contents(__DIR__ . '/Resources/valid-signed-adfs.xml')); 44 | $parameters = [ 45 | 'SAMLRequest' => base64_encode($samlRequest), 46 | 'RelayState' => '/index.php', 47 | ]; 48 | $parsed = ReceivedAuthnRequestPost::parse($parameters); 49 | $this->assertInstanceOf(ReceivedAuthnRequestPost::class, $parsed); 50 | } 51 | 52 | #[Test] 53 | public function it_can_decode_an_usigned_saml_request(): void 54 | { 55 | $samlRequest = str_replace(PHP_EOL, '', file_get_contents(__DIR__ . '/Resources/valid-unsigned.xml')); 56 | $parameters = [ 57 | 'SAMLRequest' => base64_encode($samlRequest), 58 | 'RelayState' => '/index.php', 59 | ]; 60 | $authnRequest = ReceivedAuthnRequestPost::parse($parameters); 61 | $this->assertEquals('/index.php', $authnRequest->getRelayState()); 62 | } 63 | 64 | #[Test] 65 | public function it_rejects_malformed_saml_request(): void 66 | { 67 | $this->expectExceptionMessage("Failed decoding SAML request, did not receive a valid base64 string"); 68 | $this->expectException(InvalidRequestException::class); 69 | $parameters = [ 70 | 'SAMLRequest' => 'this=notvalid==', 71 | 'RelayState' => '/index.php', 72 | ]; 73 | ReceivedAuthnRequestPost::parse($parameters); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Resources/invalid-malformed-signature-value.xml: -------------------------------------------------------------------------------- 1 | http://localhost:8989/simplesaml/module.php/saml/sp/metadata.php/sfo-sp 2 | 3 | 4 | 3j6hSACdbzubiAWM12fpzTuRWgUWgSwxZseXDhQzGXw=This=not-a-valid-base64-encoded-signature-value== 5 | MIIDmTCCAoGgAwIBAgIJAKyUXzwGwcqhMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAk5MMRAwDgYDVQQKDAdFeGFtcGxlMR4wHAYDVQQDDBVTRk8gRGVtbyBTQU1MIHNpZ25pbmcxIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRAZXhhbXBsZS5vcmcwHhcNMTYxMTA2MDgxOTIzWhcNMjYxMTA0MDgxOTIzWjBjMQswCQYDVQQGEwJOTDEQMA4GA1UECgwHRXhhbXBsZTEeMBwGA1UEAwwVU0ZPIERlbW8gU0FNTCBzaWduaW5nMSIwIAYJKoZIhvcNAQkBFhNzdXBwb3J0QGV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA03h5x2cV6+JKLYHO2BxHhiYoRQi/vMQHW7EHRTyfAptf+1AwuDT83LF4OA82oXw1PTO9ffkb9beFHMxBkHQ7fI7Qq4jjhw9ljtB7BPdN9S+uOhNPAhFHb0hHAIngCGg82PEi9hD18lPfS8OJIK+cSOgrCp2H5N2vel1yRXm4laCc8/nssoIoAkV6wnATBE3oSyDMKpK+evUz/oltryf7iLvfnB8XdP3dDMERaOFqstKrj50SCpMpA6AsKZ674aIHuvO/dUD0v5+UVnDjGl2Pbfz0vp+KhV8sWSQ6oBE44yxpYQBiHJi+1Wq0Vi4Vf+hZjiH4fI+qp2BmV0HAOD0mbwIDAQABo1AwTjAdBgNVHQ4EFgQU2em7W0TJzKoNNV3LNoVHeJaJpG0wHwYDVR0jBBgwFoAU2em7W0TJzKoNNV3LNoVHeJaJpG0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALBmM0fMx8fnNabWIIHsElk6qVGpJ6+4583pYoNT/nXrf/Lx2jwYhyyHTdFONMoHbobY0e28t4sao8GqprGFynHs5ssjhOWpADAYHV2l0lcAt0YISmRbSJk7SfHGNYr4JHI+wgt4Cfwlw6BUsVdiBM0gxFPPQrLMoPmY4ZgQoV3YyJKvq6AhhxGvyl5b54wfaEDmGuANfDSz4c3xAX8KxIOTNevUToyMY3Z2uwwEqHSyp0ayjsMoPsZymKUoNzwHQrWyGd2glqukHEPZuP0ZeHLL6dc6/zVhHt+Pwbrvq2Q1aOfiWfLljYZZZ5PNxMEXsh2ZHTkvw2IA/pYVd59Gabg==urn:collab:person:example.org:studenthttp://pilot.surfconext.nl/assurance/sfo-level2 -------------------------------------------------------------------------------- /src/Security/Authentication/SamlInteractionProvider.php: -------------------------------------------------------------------------------- 1 | samlAuthenticationStateHandler->hasRequestId(); 45 | } 46 | 47 | public function initiateSamlRequest(): RedirectResponse 48 | { 49 | $authnRequest = AuthnRequestFactory::createNewRequest( 50 | $this->serviceProvider, 51 | $this->identityProvider 52 | ); 53 | 54 | $this->samlAuthenticationStateHandler->setRequestId($authnRequest->getRequestId()); 55 | 56 | return $this->redirectBinding->createResponseFor($authnRequest); 57 | } 58 | 59 | public function processSamlResponse(Request $request): Assertion 60 | { 61 | $assertion = $this->postBinding->processResponse( 62 | $request, 63 | $this->identityProvider, 64 | $this->serviceProvider 65 | ); 66 | 67 | if ($assertion->getIssuer()->getValue() !== $this->identityProvider->getEntityId()) { 68 | throw new UnexpectedIssuerException(sprintf( 69 | 'Expected issuer to be configured remote IdP "%s", got "%s"', 70 | $this->identityProvider->getEntityId(), 71 | $assertion->getIssuer()->getValue() 72 | )); 73 | } 74 | return $assertion; 75 | } 76 | 77 | /** 78 | * Resets the SAML flow. 79 | */ 80 | public function reset(): void 81 | { 82 | $this->samlAuthenticationStateHandler->clearRequestId(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/SAML2/Extensions/GsspUserAttributesChunk.php: -------------------------------------------------------------------------------- 1 | createElementNS('urn:mace:surf.nl:stepup:gssp-extensions', 'gssp:UserAttributes'); 30 | $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); 31 | $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xs', 'http://www.w3.org/2001/XMLSchema'); 32 | 33 | if ($value && $value->hasChildNodes()) { 34 | foreach ($value->childNodes as $child) { 35 | $root->appendChild($doc->importNode($child->cloneNode(true), true)); 36 | } 37 | } 38 | 39 | $doc->appendChild($doc->importNode($root, true)); 40 | parent::__construct('UserAttributes', 'urn:mace:surf.nl:stepup:gssp-extensions', $doc->documentElement); 41 | } 42 | 43 | public function getAttributeValue(string $attributeName): ?string 44 | { 45 | $xpath = sprintf( 46 | 'saml_assertion:Attribute[@Name="%s"]/saml_assertion:AttributeValue', 47 | $attributeName 48 | ); 49 | $result = Utils::xpQuery($this->getValue(), $xpath); 50 | return count($result) ? $result[0]->textContent : null; 51 | } 52 | 53 | public function addAttribute(string $name, string $format, string $value): void 54 | { 55 | $doc = new DOMDocument("1.0", "UTF-8"); 56 | $attrib = $doc->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Attribute'); 57 | $attrib->setAttribute('NameFormat', $format); 58 | $attrib->setAttribute('Name', $name); 59 | 60 | $attribValue = $doc->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeValue', $value); 61 | $attribValue->setAttribute('xsi:type', 'xs:string'); 62 | 63 | $attrib->appendChild($attribValue); 64 | $doc->appendChild($attrib); 65 | $this->append($doc->documentElement); 66 | } 67 | 68 | public function toXML() 69 | { 70 | return $this->getValue()->ownerDocument->saveXML(); 71 | } 72 | 73 | public static function fromXML(string $xmlString): GsspUserAttributesChunk 74 | { 75 | $doc = new DOMDocument(); 76 | $doc->loadXML($xmlString); 77 | return new self($doc->documentElement); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/daily-security-check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Daily security check 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | security: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v2 15 | 16 | # PHP checks 17 | - name: Check for php composer project 18 | id: check_composer 19 | uses: andstor/file-existence-action@v2 20 | with: 21 | files: "composer.lock" 22 | - name: Run php local security checker 23 | if: steps.check_composer.outputs.files_exists == 'true' 24 | uses: symfonycorp/security-checker-action@v4 25 | 26 | # node-yarn checks 27 | - name: Check for node-yarn project 28 | id: check_node_yarn 29 | uses: andstor/file-existence-action@v2 30 | with: 31 | files: "yarn.lock" 32 | - name: Setup node 33 | if: steps.check_node_yarn.outputs.files_exists == 'true' 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 14 37 | - name: Yarn Audit 38 | if: steps.check_node_yarn.outputs.files_exists == 'true' 39 | run: yarn audit --level high --groups dependencies optionalDependencies 40 | 41 | # node-npm checks 42 | - name: Check for node-npm project 43 | id: check_node_npm 44 | uses: andstor/file-existence-action@v2 45 | with: 46 | files: "package.lock" 47 | - name: Setup node 48 | if: steps.check_node_npm.outputs.files_exists == 'true' 49 | uses: actions/setup-node@v3 50 | with: 51 | node-version: 14 52 | - name: npm audit 53 | if: steps.check_node_npm.outputs.files_exists == 'true' 54 | run: npm audit --audit-level=high 55 | 56 | # python checks 57 | - name: Check for python project 58 | id: check_python 59 | uses: andstor/file-existence-action@v2 60 | with: 61 | files: "requirements.txt" 62 | - name: Safety checks Python dependencies 63 | if: steps.check_python.outputs.files_exists == 'true' 64 | uses: pyupio/safety@2.3.5 65 | 66 | # java checks 67 | - name: Check for java maven project 68 | id: check_maven 69 | uses: andstor/file-existence-action@v2 70 | with: 71 | files: "pom.xml" 72 | - name: Setup java if needed 73 | if: steps.check_maven.outputs.files_exists == 'true' 74 | uses: actions/setup-java@v1 75 | with: 76 | java-version: 11 77 | - name: Check java 78 | if: steps.check_maven.outputs.files_exists == 'true' 79 | run: mvn org.owasp:dependency-check-maven:check 80 | 81 | # Send results 82 | - name: Send to Slack if something failed 83 | if: failure() 84 | uses: rtCamp/action-slack-notify@v2 85 | env: 86 | SLACK_CHANNEL: surfconext-nightly-check 87 | SLACK_COLOR: ${{ job.status }} 88 | SLACK_MESSAGE: 'Dependency check failed :crying_cat_face:' 89 | SLACK_TITLE: Dependency check wants attention 90 | SLACK_USERNAME: NightlySecurityCheck 91 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 92 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Resources/invalid-missing-signing-algorithm.xml: -------------------------------------------------------------------------------- 1 | http://localhost:8989/simplesaml/module.php/saml/sp/metadata.php/sfo-sp 2 | 3 | 3j6hSACdbzubiAWM12fpzTuRWgUWgSwxZseXDhQzGXw=RggbPG+XX01ftI4dMY/sAbrxV009CT6GcSTwSsR3qxt5tCdO91Xl07gkoSuRQ6Dn1pJNgNN2/cmWy3bMCsRrega43QXJAe4elW42OE6FIGlvGHLLbizKH3bwhGJVM65kFGuAHo077ao9/YLobCJeoFbDicNTPVt0aWb6ZdxWlhjaAIszFKQqdBPLAnHjRQw76WpgoBM+nNKzR8MbU4H6V94/pAbfNY67iQscN+iu9B8SIRkiGGpSw8155l7MjWGwzBUZUDp7rBW8eRzVR3NDS1jkiQ1GSqkQqsdi1Iy8nRRDdNRvhYQzlgjCNdV5LpoaWGoP21+8nlRZao3jRvdo5w== 4 | MIIDmTCCAoGgAwIBAgIJAKyUXzwGwcqhMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAk5MMRAwDgYDVQQKDAdFeGFtcGxlMR4wHAYDVQQDDBVTRk8gRGVtbyBTQU1MIHNpZ25pbmcxIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRAZXhhbXBsZS5vcmcwHhcNMTYxMTA2MDgxOTIzWhcNMjYxMTA0MDgxOTIzWjBjMQswCQYDVQQGEwJOTDEQMA4GA1UECgwHRXhhbXBsZTEeMBwGA1UEAwwVU0ZPIERlbW8gU0FNTCBzaWduaW5nMSIwIAYJKoZIhvcNAQkBFhNzdXBwb3J0QGV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA03h5x2cV6+JKLYHO2BxHhiYoRQi/vMQHW7EHRTyfAptf+1AwuDT83LF4OA82oXw1PTO9ffkb9beFHMxBkHQ7fI7Qq4jjhw9ljtB7BPdN9S+uOhNPAhFHb0hHAIngCGg82PEi9hD18lPfS8OJIK+cSOgrCp2H5N2vel1yRXm4laCc8/nssoIoAkV6wnATBE3oSyDMKpK+evUz/oltryf7iLvfnB8XdP3dDMERaOFqstKrj50SCpMpA6AsKZ674aIHuvO/dUD0v5+UVnDjGl2Pbfz0vp+KhV8sWSQ6oBE44yxpYQBiHJi+1Wq0Vi4Vf+hZjiH4fI+qp2BmV0HAOD0mbwIDAQABo1AwTjAdBgNVHQ4EFgQU2em7W0TJzKoNNV3LNoVHeJaJpG0wHwYDVR0jBBgwFoAU2em7W0TJzKoNNV3LNoVHeJaJpG0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALBmM0fMx8fnNabWIIHsElk6qVGpJ6+4583pYoNT/nXrf/Lx2jwYhyyHTdFONMoHbobY0e28t4sao8GqprGFynHs5ssjhOWpADAYHV2l0lcAt0YISmRbSJk7SfHGNYr4JHI+wgt4Cfwlw6BUsVdiBM0gxFPPQrLMoPmY4ZgQoV3YyJKvq6AhhxGvyl5b54wfaEDmGuANfDSz4c3xAX8KxIOTNevUToyMY3Z2uwwEqHSyp0ayjsMoPsZymKUoNzwHQrWyGd2glqukHEPZuP0ZeHLL6dc6/zVhHt+Pwbrvq2Q1aOfiWfLljYZZZ5PNxMEXsh2ZHTkvw2IA/pYVd59Gabg==urn:collab:person:example.org:studenthttp://pilot.surfconext.nl/assurance/sfo-level2 -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Resources/valid-signed.xml: -------------------------------------------------------------------------------- 1 | http://localhost:8989/simplesaml/module.php/saml/sp/metadata.php/sfo-sp 2 | 3 | 4 | 3j6hSACdbzubiAWM12fpzTuRWgUWgSwxZseXDhQzGXw=RggbPG+XX01ftI4dMY/sAbrxV009CT6GcSTwSsR3qxt5tCdO91Xl07gkoSuRQ6Dn1pJNgNN2/cmWy3bMCsRrega43QXJAe4elW42OE6FIGlvGHLLbizKH3bwhGJVM65kFGuAHo077ao9/YLobCJeoFbDicNTPVt0aWb6ZdxWlhjaAIszFKQqdBPLAnHjRQw76WpgoBM+nNKzR8MbU4H6V94/pAbfNY67iQscN+iu9B8SIRkiGGpSw8155l7MjWGwzBUZUDp7rBW8eRzVR3NDS1jkiQ1GSqkQqsdi1Iy8nRRDdNRvhYQzlgjCNdV5LpoaWGoP21+8nlRZao3jRvdo5w== 5 | MIIDmTCCAoGgAwIBAgIJAKyUXzwGwcqhMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAk5MMRAwDgYDVQQKDAdFeGFtcGxlMR4wHAYDVQQDDBVTRk8gRGVtbyBTQU1MIHNpZ25pbmcxIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRAZXhhbXBsZS5vcmcwHhcNMTYxMTA2MDgxOTIzWhcNMjYxMTA0MDgxOTIzWjBjMQswCQYDVQQGEwJOTDEQMA4GA1UECgwHRXhhbXBsZTEeMBwGA1UEAwwVU0ZPIERlbW8gU0FNTCBzaWduaW5nMSIwIAYJKoZIhvcNAQkBFhNzdXBwb3J0QGV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA03h5x2cV6+JKLYHO2BxHhiYoRQi/vMQHW7EHRTyfAptf+1AwuDT83LF4OA82oXw1PTO9ffkb9beFHMxBkHQ7fI7Qq4jjhw9ljtB7BPdN9S+uOhNPAhFHb0hHAIngCGg82PEi9hD18lPfS8OJIK+cSOgrCp2H5N2vel1yRXm4laCc8/nssoIoAkV6wnATBE3oSyDMKpK+evUz/oltryf7iLvfnB8XdP3dDMERaOFqstKrj50SCpMpA6AsKZ674aIHuvO/dUD0v5+UVnDjGl2Pbfz0vp+KhV8sWSQ6oBE44yxpYQBiHJi+1Wq0Vi4Vf+hZjiH4fI+qp2BmV0HAOD0mbwIDAQABo1AwTjAdBgNVHQ4EFgQU2em7W0TJzKoNNV3LNoVHeJaJpG0wHwYDVR0jBBgwFoAU2em7W0TJzKoNNV3LNoVHeJaJpG0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALBmM0fMx8fnNabWIIHsElk6qVGpJ6+4583pYoNT/nXrf/Lx2jwYhyyHTdFONMoHbobY0e28t4sao8GqprGFynHs5ssjhOWpADAYHV2l0lcAt0YISmRbSJk7SfHGNYr4JHI+wgt4Cfwlw6BUsVdiBM0gxFPPQrLMoPmY4ZgQoV3YyJKvq6AhhxGvyl5b54wfaEDmGuANfDSz4c3xAX8KxIOTNevUToyMY3Z2uwwEqHSyp0ayjsMoPsZymKUoNzwHQrWyGd2glqukHEPZuP0ZeHLL6dc6/zVhHt+Pwbrvq2Q1aOfiWfLljYZZZ5PNxMEXsh2ZHTkvw2IA/pYVd59Gabg==urn:collab:person:example.org:studenthttp://pilot.surfconext.nl/assurance/sfo-level2 -------------------------------------------------------------------------------- /src/Security/Authentication/Handler/ProcessSamlAuthenticationHandler.php: -------------------------------------------------------------------------------- 1 | authenticationStateHandler->getRequestId(); 44 | $logger = $this->authenticationLogger->forAuthentication($expectedInResponseTo); 45 | 46 | $logger->notice('No authenticated user and AuthnRequest pending, attempting to process SamlResponse'); 47 | 48 | try { 49 | $assertion = $this->samlInteractionProvider->processSamlResponse($request); 50 | } catch (AuthnFailedSamlResponseException $exception) { 51 | $logger->notice(sprintf('SAML Authentication failed at IdP: "%s"', $exception->getMessage())); 52 | throw new AuthenticationException('Failed SAMLResponse parsing', 0, $exception); 53 | } catch (PreconditionNotMetException $exception) { 54 | $logger->notice(sprintf('SAMLResponse precondition not met: "%s"', $exception->getMessage())); 55 | throw new AuthenticationException('Failed SAMLResponse parsing', 0, $exception); 56 | } catch (Exception $exception) { 57 | $logger->error(sprintf('Failed SAMLResponse Parsing: "%s"', $exception->getMessage())); 58 | throw new AuthenticationException('Failed SAMLResponse parsing', 0, $exception); 59 | } 60 | if (!InResponseTo::assertEquals($assertion, $expectedInResponseTo)) { 61 | $logger->error('Unknown or unexpected InResponseTo in SAMLResponse'); 62 | throw new AuthenticationException('Unknown or unexpected InResponseTo in SAMLResponse'); 63 | } 64 | return $assertion; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Tests/Component/Metadata/keys/entity.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | 1ST74GttSoJZvmS5PD8DEpT2lOQYbRt5 52 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /src/Value/DateTime.php: -------------------------------------------------------------------------------- 1 | dateTime = $dateTime ?: new CoreDateTime(); 60 | } 61 | 62 | public function add(DateInterval $interval): self 63 | { 64 | $dateTime = clone $this->dateTime; 65 | $dateTime->add($interval); 66 | 67 | return new self($dateTime); 68 | } 69 | 70 | public function sub(DateInterval $interval): self 71 | { 72 | $dateTime = clone $this->dateTime; 73 | $dateTime->sub($interval); 74 | 75 | return new self($dateTime); 76 | } 77 | 78 | public function comesBefore(DateTime $dateTime): bool 79 | { 80 | return $this->dateTime < $dateTime->dateTime; 81 | } 82 | 83 | public function comesBeforeOrIsEqual(DateTime $dateTime): bool 84 | { 85 | return $this->dateTime <= $dateTime->dateTime; 86 | } 87 | 88 | public function comesAfter(DateTime $dateTime): bool 89 | { 90 | return $this->dateTime > $dateTime->dateTime; 91 | } 92 | 93 | public function comesAfterOrIsEqual(DateTime $dateTime): bool 94 | { 95 | return $this->dateTime >= $dateTime->dateTime; 96 | } 97 | 98 | public function format(string $format): string 99 | { 100 | return $this->dateTime->format($format); 101 | } 102 | 103 | /** 104 | * @return string An ISO 8601 representation of this DateTime. 105 | */ 106 | public function __toString(): string 107 | { 108 | return $this->format(self::FORMAT); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # UPGRADE FROM X to 4.1.9 2 | 3 | When using this bundle with Symfony 4.3 you should configure the templating engine: 4 | 5 | ```yaml 6 | framework: 7 | templating: 8 | engines: 9 | - twig 10 | ``` 11 | 12 | # UPGRADE FROM 3.X to 4.X 13 | This release makes error reporting more specific. This release changed the API of the 14 | `ReceivedAuthnRequestQueryString::getSignatureAlgorithm` method, returning the signature algorithm url decoded. Any 15 | code using this method should be updated removing the url_decode call to prevent double decoding of the sigalg value. 16 | 17 | # UPGRADE FROM 2.X to 3.X 18 | 19 | ## SimpleSamlPHP SAML2 20 | The most noticable change is the upgrade of the `simplesamlphp/saml2` library upgrade from version 1 to 3. This 21 | resulted in a bundle wide upgrade of the SAML2 namespaces and the implementation of the SAML2 NameID implementaion. 22 | 23 | ### Update instruction 24 | When upgrading the library some other dependencies are to be upgraded most notable is `robrichards/xmlseclibs`. To 25 | streamline the upgrade the following installtion instructions are recommended: 26 | 27 | ``` 28 | composer remove surfnet/stepup-saml-bundle --ignore-platform-reqs 29 | composer require surfnet/stepup-saml-bundle "^3.0" --ignore-platform-reqs 30 | ``` 31 | 32 | :grey_exclamation: Simply running `composer update surfnet/stepup-saml-bundle "^3.0"` will probably fail as other 33 | dependencies will block the update of the package. 34 | 35 | ### Code changes 36 | After updating the SAML2 library, we advice you to scan your project for usages of the SAML2 library. You can do 37 | this by grepping your project for usages of the old PEAR style SAML2 classnames. 38 | 39 | **Namespace** 40 | 41 | Grep for usages `SAML2_` in your application. PEAR style class references should be updated to their PSR 42 | counterparts. Doing so is quite easy. 43 | 44 | ``` 45 | // old style 46 | use SAML2_Assertion; 47 | 48 | // new style 49 | use SAML2\Assertion; 50 | ``` 51 | 52 | **NameID** 53 | 54 | Using NameID values was changed in the SAML2 library. Instead of receiving an array representation of the NameId 55 | `['Value' => 'john_doe', 'Format' => 'unspecified')`, a value object is returned. Please inspect your project 56 | for usages of the getNameId method on assertions. 57 | 58 | **XMLSecurityKey** 59 | 60 | Finally all usages of `XMLSecurityKey` should be checked. The `XMLSecurityKey` objects are now loaded from the 61 | `RobRichards` namespace. 62 | 63 | # UPGRADE FROM 1.X to 2.X 64 | 65 | ## Multiplicity 66 | 67 | The multiplicity functionality has been removed from `Surfnet\SamlBundle\SAML2\Attribute\AttributeDefinition`. 68 | This means that the method `AttributeDefinition::getMultiplicity()` no longer exists. Furthermore, the related 69 | constants `AttributeDefinition::MULTIPLICITY_SINGLE` and `AttributeDefinition::MULTIPLICITY_MULTIPLE` have been 70 | removed. 71 | 72 | **WARNING** The value of an attribute is now always an array of strings, it can no longer be `null` or `string`. 73 | This means code relying on the values of attributes should be modified to always accept `string[]` as return value 74 | and handle accordingly. 75 | 76 | The following deprecated methods have been removed: 77 | 78 | | Class | Removed method | Replaced with | 79 | | ---------------------------------------------------- | ---------------- | --------------------- | 80 | | `Surfnet\SamlBundle\SAML2\Response\AssertionAdapter` | `getAttribute()` | `getAttributeValue()` | 81 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Response/Assertion/InResponseToTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(InResponseTo::assertEquals($assertion, null)); 42 | $this->assertFalse(InResponseTo::assertEquals($assertion, 'some not-null-value')); 43 | } 44 | 45 | #[Test] 46 | #[Group('saml2-response')] 47 | #[Group('saml2')] 48 | public function in_reponse_to_equality_is_strictly_checked(): void 49 | { 50 | $assertion = new Assertion(); 51 | $subjectConfirmationWithData = new SubjectConfirmation(); 52 | $subjectConfirmationData = new SubjectConfirmationData(); 53 | $subjectConfirmationData->setInResponseTo('1'); 54 | $subjectConfirmationWithData->setSubjectConfirmationData($subjectConfirmationData); 55 | $assertion->setSubjectConfirmation([$subjectConfirmationWithData]); 56 | 57 | $this->assertTrue(InResponseTo::assertEquals($assertion, '1')); 58 | $this->assertFalse(InResponseTo::assertEquals($assertion, 1)); 59 | } 60 | 61 | public static function provideAssertionsWithoutInResponseTo(): array 62 | { 63 | ContainerSingleton::setContainer(new MockContainer()); 64 | $assertionWithoutSubjectConfirmation = new Assertion(); 65 | 66 | $assertionWithoutSubjectConfirmationData = new Assertion(); 67 | $subjectConfirmation = new SubjectConfirmation(); 68 | $assertionWithoutSubjectConfirmationData->setSubjectConfirmation([$subjectConfirmation]); 69 | 70 | $assertionWithEmptyInResponseTo = new Assertion(); 71 | $subjectConfirmationWithData = new SubjectConfirmation(); 72 | $subjectConfirmationWithData->setSubjectConfirmationData(new SubjectConfirmationData()); 73 | $assertionWithEmptyInResponseTo->setSubjectConfirmation([$subjectConfirmationWithData]); 74 | 75 | return [ 76 | 'No Subject Confirmation' => [$assertionWithoutSubjectConfirmation], 77 | 'No Subject Confirmation Data' => [$assertionWithoutSubjectConfirmationData], 78 | 'Empty In Reponse To' => [$assertionWithEmptyInResponseTo] 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Monolog/SamlAuthenticationLogger.php: -------------------------------------------------------------------------------- 1 | logger); 44 | $logger->sari = $requestId; 45 | 46 | return $logger; 47 | } 48 | 49 | public function emergency(string|Stringable $message, array $context = []): void 50 | { 51 | $this->logger->emergency($message, $this->modifyContext($context)); 52 | } 53 | 54 | public function alert(string|Stringable $message, array $context = []): void 55 | { 56 | $this->logger->alert($message, $this->modifyContext($context)); 57 | } 58 | 59 | public function critical(string|Stringable $message, array $context = []): void 60 | { 61 | $this->logger->critical($message, $this->modifyContext($context)); 62 | } 63 | 64 | public function error(string|Stringable $message, array $context = []): void 65 | { 66 | $this->logger->error($message, $this->modifyContext($context)); 67 | } 68 | 69 | public function warning(string|Stringable $message, array $context = []): void 70 | { 71 | $this->logger->warning($message, $this->modifyContext($context)); 72 | } 73 | 74 | public function notice(string|Stringable $message, array $context = []): void 75 | { 76 | $this->logger->notice($message, $this->modifyContext($context)); 77 | } 78 | 79 | public function info(string|Stringable $message, array $context = []): void 80 | { 81 | $this->logger->info($message, $this->modifyContext($context)); 82 | } 83 | 84 | public function debug(string|Stringable $message, array $context = []): void 85 | { 86 | $this->logger->debug($message, $this->modifyContext($context)); 87 | } 88 | 89 | public function log($level, string|Stringable $message, array $context = []): void 90 | { 91 | $this->logger->log($level, $message, $this->modifyContext($context)); 92 | } 93 | 94 | /** 95 | * Adds the SARI to the log context 96 | */ 97 | private function modifyContext(array $context): array 98 | { 99 | if ($this->sari) { 100 | $context['sari'] = $this->sari; 101 | } 102 | 103 | return $context; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/AuthnRequestFactoryTest.php: -------------------------------------------------------------------------------- 1 | expectExceptionMessage("Failed decoding the request, did not receive a valid base64 string"); 43 | $this->expectException(InvalidRequestException::class); 44 | $invalidCharacter = '$'; 45 | $queryParams = [AuthnRequest::PARAMETER_REQUEST => $invalidCharacter]; 46 | $serverParams = [ 47 | 'REQUEST_URI' => sprintf('https://test.example?%s=%s', AuthnRequest::PARAMETER_REQUEST, $invalidCharacter), 48 | ]; 49 | $request = new Request($queryParams, [], [], [], [], $serverParams); 50 | 51 | AuthnRequestFactory::createFromHttpRequest($request); 52 | } 53 | 54 | #[Test] 55 | #[Group('saml2')] 56 | public function an_exception_is_thrown_when_a_request_cannot_be_inflated(): void 57 | { 58 | $this->expectExceptionMessage("Failed inflating the request;"); 59 | $this->expectException(InvalidRequestException::class); 60 | $nonDeflated = base64_encode('nope, not deflated'); 61 | $queryParams = [AuthnRequest::PARAMETER_REQUEST => $nonDeflated]; 62 | $serverParams = [ 63 | 'REQUEST_URI' => sprintf('https://test.example?%s=%s', AuthnRequest::PARAMETER_REQUEST, $nonDeflated), 64 | ]; 65 | $request = new Request($queryParams, [], [], [], [], $serverParams); 66 | 67 | AuthnRequestFactory::createFromHttpRequest($request); 68 | } 69 | 70 | #[Test] 71 | #[Group('saml2')] 72 | public function verify_force_authn_works_as_intended(): void 73 | { 74 | $sp = m::mock(ServiceProvider::class); 75 | $sp->shouldReceive('getAssertionConsumerUrl')->andReturn('https://example-sp.com/acs'); 76 | $sp->shouldReceive('getEntityId')->andReturn('https://example-sp.com/'); 77 | 78 | $pk = new PrivateKey(__DIR__.'/../../../Resources/keys/development_privatekey.pem', 'key-for-test', ''); 79 | 80 | $sp->shouldReceive('getPrivateKey')->andReturn($pk); 81 | 82 | $idp = m::mock(IdentityProvider::class); 83 | $idp->shouldReceive('getSsoUrl')->andReturn('https://example-idp.com/sso'); 84 | 85 | $authnRequest = AuthnRequestFactory::createNewRequest($sp, $idp, true); 86 | $this->assertTrue($authnRequest->isForceAuthn()); 87 | $authnRequest = AuthnRequestFactory::createNewRequest($sp, $idp, false); 88 | $this->assertFalse($authnRequest->isForceAuthn()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Entity/HostedEntities.php: -------------------------------------------------------------------------------- 1 | serviceProvider)) { 43 | return $this->serviceProvider; 44 | } 45 | 46 | if (is_null($this->serviceProviderConfiguration) || 47 | !array_key_exists('enabled', $this->serviceProviderConfiguration) 48 | ) { 49 | return null; 50 | } 51 | 52 | $configuration = $this->createStandardEntityConfiguration($this->serviceProviderConfiguration); 53 | $configuration['assertionConsumerUrl'] = $this->generateUrl( 54 | $this->serviceProviderConfiguration['assertion_consumer_route'] 55 | ); 56 | 57 | return $this->serviceProvider = new ServiceProvider($configuration); 58 | } 59 | 60 | public function getIdentityProvider(): ?IdentityProvider 61 | { 62 | if (!empty($this->identityProvider)) { 63 | return $this->identityProvider; 64 | } 65 | 66 | if (is_null($this->identityProviderConfiguration) || 67 | !array_key_exists('enabled', $this->identityProviderConfiguration) 68 | ) { 69 | return null; 70 | } 71 | 72 | $configuration = $this->createStandardEntityConfiguration($this->identityProviderConfiguration); 73 | $configuration['ssoUrl'] = $this->generateUrl( 74 | $this->identityProviderConfiguration['sso_route'] 75 | ); 76 | 77 | return $this->identityProvider = new IdentityProvider($configuration); 78 | } 79 | 80 | private function createStandardEntityConfiguration(array $entityConfiguration): array 81 | { 82 | $privateKey = new PrivateKey($entityConfiguration['private_key'], PrivateKey::NAME_DEFAULT); 83 | 84 | return [ 85 | 'entityId' => $this->generateUrl($entityConfiguration['entity_id_route']), 86 | 'certificateFile' => $entityConfiguration['public_key'], 87 | 'privateKeys' => [$privateKey], 88 | 'blacklistedAlgorithms' => [], 89 | 'assertionEncryptionEnabled' => false 90 | ]; 91 | } 92 | 93 | /** 94 | * @param string|array $routeDefinition 95 | */ 96 | private function generateUrl(string|array $routeDefinition): string 97 | { 98 | $route = is_array($routeDefinition) ? $routeDefinition['route'] : $routeDefinition; 99 | $parameters = is_array($routeDefinition) ? $routeDefinition['parameters'] : []; 100 | 101 | $context = $this->router->getContext(); 102 | $context->fromRequest($this->requestStack->getMainRequest()); 103 | $url = $this->router->generate($route, $parameters, RouterInterface::ABSOLUTE_URL); 104 | 105 | $context->fromRequest($this->requestStack->getCurrentRequest()); 106 | 107 | return $url; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Tests/Unit/SAML2/Attribute/ConfigurableAttributeSetFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(self::DUMMY_ATTRIBUTE_SET_CLASS, $attributeSet); 53 | } 54 | 55 | #[Test] 56 | #[Group('AssertionAdapter')] 57 | #[Group('AttributeSet')] 58 | public function which_attribute_set_is_created_from_attributes_is_configurable(): void 59 | { 60 | ConfigurableAttributeSetFactory::configureWhichAttributeSetToCreate(self::DUMMY_ATTRIBUTE_SET_CLASS); 61 | $attributeSet = ConfigurableAttributeSetFactory::create([]); 62 | ConfigurableAttributeSetFactory::configureWhichAttributeSetToCreate(AttributeSet::class); 63 | 64 | $this->assertInstanceOf(self::DUMMY_ATTRIBUTE_SET_CLASS, $attributeSet); 65 | } 66 | 67 | #[Test] 68 | #[DataProvider('nonOrEmptyStringProvider')] 69 | #[Group('AssertionAdapter')] 70 | #[Group('AttributeSet')] 71 | public function the_attribute_set_to_use_can_only_be_represented_as_a_non_empty_string(int|float|bool|array|stdClass|string $nonOrEmptyString): void 72 | { 73 | $this->expectException(InvalidArgumentException::class); 74 | $this->expectExceptionMessage('non-empty string'); 75 | 76 | ConfigurableAttributeSetFactory::configureWhichAttributeSetToCreate($nonOrEmptyString); 77 | } 78 | 79 | #[Test] 80 | #[Group('AssertionAdapter')] 81 | #[Group('AttributeSet')] 82 | public function the_attribute_set_to_use_has_to_implement_attribute_set_factory(): void 83 | { 84 | $this->expectException(InvalidArgumentException::class); 85 | $this->expectExceptionMessage('implement'); 86 | 87 | ConfigurableAttributeSetFactory::configureWhichAttributeSetToCreate('Non\Existent\Class'); 88 | } 89 | 90 | public static function nonOrEmptyStringProvider(): array 91 | { 92 | return [ 93 | 'integer' => [1], 94 | 'float' => [1.23], 95 | 'boolean' => [true], 96 | 'array' => [[]], 97 | 'object' => [new stdClass], 98 | 'empty string' => [''], 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/SAML2/Attribute/AttributeSet.php: -------------------------------------------------------------------------------- 1 | getAttributes() as $urn => $attributeValue) { 40 | try { 41 | $attribute = new Attribute( 42 | $attributeDictionary->getAttributeDefinitionByUrn($urn), 43 | $attributeValue 44 | ); 45 | $attributeSet->initializeWith($attribute); 46 | } catch (UnknownUrnException $e) { 47 | if (!$attributeDictionary->ignoreUnknownAttributes()) { 48 | throw $e; 49 | } 50 | } 51 | } 52 | 53 | return $attributeSet; 54 | } 55 | 56 | public static function create(array $attributes): AttributeSet 57 | { 58 | $attributeSet = new AttributeSet(); 59 | 60 | foreach ($attributes as $attribute) { 61 | $attributeSet->initializeWith($attribute); 62 | } 63 | 64 | return $attributeSet; 65 | } 66 | 67 | private function __construct() 68 | { 69 | } 70 | 71 | public function apply(AttributeFilter $attributeFilter): AttributeSet 72 | { 73 | return self::create(array_filter($this->attributes, $attributeFilter->allows(...))); 74 | } 75 | 76 | public function getAttributeByDefinition(AttributeDefinition $attributeDefinition): Attribute 77 | { 78 | foreach ($this->attributes as $attribute) { 79 | if ($attributeDefinition->equals($attribute->getAttributeDefinition())) { 80 | return $attribute; 81 | } 82 | } 83 | 84 | throw new RuntimeException(sprintf( 85 | 'Attempted to get unknown attribute defined by "%s"', 86 | $attributeDefinition->getName() 87 | )); 88 | } 89 | 90 | public function containsAttributeDefinedBy(AttributeDefinition $attributeDefinition): bool 91 | { 92 | foreach ($this->attributes as $attribute) { 93 | if ($attributeDefinition->equals($attribute->getAttributeDefinition())) { 94 | return true; 95 | } 96 | } 97 | 98 | return false; 99 | } 100 | 101 | public function contains(Attribute $otherAttribute): bool 102 | { 103 | foreach ($this->attributes as $attribute) { 104 | if ($attribute->equals($otherAttribute)) { 105 | return true; 106 | } 107 | } 108 | 109 | return false; 110 | } 111 | 112 | public function getIterator(): Traversable 113 | { 114 | return new ArrayIterator($this->attributes); 115 | } 116 | 117 | public function count(): int 118 | { 119 | return count($this->attributes); 120 | } 121 | 122 | protected function initializeWith(Attribute $attribute): void 123 | { 124 | if ($this->contains($attribute)) { 125 | return; 126 | } 127 | 128 | $this->attributes[] = $attribute; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Tests/Unit/Mock/saml-assertion.txt: -------------------------------------------------------------------------------- 1 | 7 | https://idp-dev.stepup.coin.surf.net/saml2/idp/metadata.php 8 | 9 | 6baae2e409df7f0e9fd8f0c3b73bc61d0bedc9d7 12 | 13 | 14 | 18 | 19 | 20 | 23 | 24 | https://gw-dev.stepup.coin.surf.net/app_dev.php/authentication/metadata 25 | 26 | 27 | 31 | 32 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 33 | 34 | 35 | 36 | 39 | foobar 40 | 41 | 44 | 3858f62230ac3c915f300c664312c63f 45 | 46 | 49 | Foo Bar 50 | 51 | 54 | foo@bar.com 55 | 56 | 59 | foo@bar.com 60 | 61 | 64 | Example Inc 65 | 66 | 69 | 70 | 904e2fdaa9a2870d6cf5fb6c81fd0511ce6eefeb 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/SAML2/Attribute/AttributeDictionary.php: -------------------------------------------------------------------------------- 1 | ignoreUnknownAttributes; 58 | } 59 | 60 | /** 61 | * @param AttributeDefinition $attributeDefinition 62 | * 63 | * We store the definitions indexed both by name and by urn to ensure speedy lookups due to the amount of 64 | * definitions and the amount of usages of the lookups 65 | */ 66 | public function addAttributeDefinition(AttributeDefinition $attributeDefinition): void 67 | { 68 | if (isset($this->attributeDefinitionsByName[$attributeDefinition->getName()])) { 69 | throw new LogicException(sprintf( 70 | 'Cannot add attribute "%s" as it has already been added', 71 | $attributeDefinition->getName() 72 | )); 73 | } 74 | 75 | $this->attributeDefinitionsByName[$attributeDefinition->getName()] = $attributeDefinition; 76 | 77 | if ($attributeDefinition->hasUrnMace()) { 78 | $this->attributeDefinitionsByUrn[$attributeDefinition->getUrnMace()] = $attributeDefinition; 79 | } 80 | 81 | if ($attributeDefinition->hasUrnOid()) { 82 | $this->attributeDefinitionsByUrn[$attributeDefinition->getUrnOid()] = $attributeDefinition; 83 | } 84 | } 85 | 86 | public function translate(Assertion $assertion): AssertionAdapter 87 | { 88 | return new AssertionAdapter($assertion, $this); 89 | } 90 | 91 | public function hasAttributeDefinition(string $attributeName): bool 92 | { 93 | return isset($this->attributeDefinitionsByName[$attributeName]); 94 | } 95 | 96 | public function getAttributeDefinition(string $attributeName): AttributeDefinition 97 | { 98 | if (!$this->hasAttributeDefinition($attributeName)) { 99 | throw new LogicException(sprintf( 100 | 'Cannot get AttributeDefinition "%s" as it has not been added to the collection', 101 | $attributeName 102 | )); 103 | } 104 | 105 | return $this->attributeDefinitionsByName[$attributeName]; 106 | } 107 | 108 | public function findAttributeDefinitionByUrn(string $urn): ?AttributeDefinition 109 | { 110 | if ($urn === '') { 111 | throw InvalidArgumentException::invalidType('non-empty string', $urn, 'urn'); 112 | } 113 | 114 | if (array_key_exists($urn, $this->attributeDefinitionsByUrn)) { 115 | return $this->attributeDefinitionsByUrn[$urn]; 116 | } 117 | 118 | return null; 119 | } 120 | 121 | public function getAttributeDefinitionByUrn(string $urn): AttributeDefinition 122 | { 123 | if (array_key_exists($urn, $this->attributeDefinitionsByUrn)) { 124 | return $this->attributeDefinitionsByUrn[$urn]; 125 | } 126 | 127 | throw new UnknownUrnException($urn); 128 | } 129 | } 130 | --------------------------------------------------------------------------------