├── .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 | MIIFlDCCA3ygAwIBAgIBADANBgkqhkiG9w0BAQsFADA4MRAwDgYDVQQDDAdSb290
3 | IENBMRcwFQYDVQQKDA5EZXZlbG9wbWVudCBWTTELMAkGA1UEBhMCTkwwHhcNMjEw
4 | NDIwMDcwMjI3WhcNMzEwNDE4MDcwMjI3WjA4MRAwDgYDVQQDDAdSb290IENBMRcw
5 | FQYDVQQKDA5EZXZlbG9wbWVudCBWTTELMAkGA1UEBhMCTkwwggIiMA0GCSqGSIb3
6 | DQEBAQUAA4ICDwAwggIKAoICAQDFnQ0Eu46gLACidX5S9V/+Jr09KOahJi/kPhI2
7 | /KcL/dYQUxdkiVzoyLEey0KWaT/Pr5MtPwz60NFGKJmcfanf3Rt7awuxmnSSxqgR
8 | 8bbdEDnNnTsOko2KIpCKNSUjVtGGQnnEwPD12/MKkiy9COyS/m3dCfeEapGLONyn
9 | L7jrse/+cN80/1Llf0Z2FcCl9h5Q6hiF6hjoFMySlsg5g2IGelRqaaiQz8vrzb1c
10 | 14a+vZyIk4W/jz/lEVA2AIg6MnQ2cZ4I4jUrSymOzRUFGZKsgvHoKNTALYSuzYuj
11 | GtK8d4F0QjRCgx5NrRwXoIRIvtJXV+v1FXeBJevZu9XWtFtmX8TiXqwlVpprev/3
12 | FdLk/lS1Ntakkw2Uf7H02g81NlM3Yr1dWg7B7uHTiAkLm6OkL47ciVtp/W7zPW8i
13 | AsIb1vZp9aBlfylzu+tskQ4xGFy1K9cxIdKqgKRweqe2K8MxFIHpdALunz4PDEu6
14 | 6KCQiTWxJ0QQ3Va9w/3SZZGlWJ7TMGq9cbCJA1F+aabV0SRfz7anua3D1qi8dH/e
15 | LTkapy3TZ3NNja6zJgavuojYmhlGccg5eS+XPsKT3mAzNtXYGD4KiGkQ8dfREBm7
16 | nJs3uIWnAAlgVIc00FWNz7kCeEw4PYXLkA5eR4IHfQdfiatHW1Yd7WCBqXgRO2bc
17 | YLBCswIDAQABo4GoMIGlMB0GA1UdDgQWBBTXdSRw4Pa/a+Osplza4xe8lCK+BTBg
18 | BgNVHSMEWTBXgBTXdSRw4Pa/a+Osplza4xe8lCK+BaE8pDowODEQMA4GA1UEAwwH
19 | Um9vdCBDQTEXMBUGA1UECgwORGV2ZWxvcG1lbnQgVk0xCzAJBgNVBAYTAk5MggEA
20 | MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB
21 | CwUAA4ICAQCu63krkaCj41Py5oM1G0r+ncSB1szYC3u1Sbwjq2pDnUWSHN6SPFHj
22 | kQ73apPl0K52LlMazNIWX/D4avJ7Jfhk5QVY0/KaHInj9pvBw30Nkixc9qdbB4pi
23 | 2U3aLxgLRdBLmtG5WqPmpyMXbXYK5tZCgHMvw6fNMWIAoIYLDXbcMfwyqf0V8S07
24 | gKUvgYTZaCsQ4BgheEVL7nfGJnwF9E0DIwmUxr/KoilMJrC3lD/O/YDqIXgNVK8x
25 | y5ToB2emTkR729GSp2bvPHnyC9kllI1OxKpK2pW4RQBNzfcJ45DmbdzaB38Mh1ot
26 | Alycu6ObEL0txox8APm0e5iqNiUlFqhOjVMEHqC94DfJpOOLBNG8ZaHyfYf+rOSx
27 | Jv2ctB9Zyklgxa3x2gCQjF/C04QGba5xJLQTNOatKBZrGczS2tv5FSfcZrQiajZM
28 | 4nDX3z1eWL+jvd3n+aYkaQUzQybzk9fqil7lMp+BeZjLnXD7CqweECQl0R/V1Lgj
29 | txqftCrtwSbgbIF+LBR+M/mIja/Di4Mt5wMfCVpy8wJa0gkxXbX1IsdppDrMcZ43
30 | ZjpUgRRm3+sRuWPVil8vwqeQ1FriNVqJEjLZN9vm3by8i/b/5g48Egc2IGv21exR
31 | /3o2lf0zVLl83qVP9JUJQKb3tMqUMq0H8SnxrLz+bnKT+u6gfP44ww==
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 |
--------------------------------------------------------------------------------