diff --git a/Dockerfile b/Dockerfile index 1c701fdb..d8949c71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,7 +56,7 @@ COPY dockerbuild/ssp-overrides/id.php $SSP_PATH/www/id.php COPY dockerbuild/ssp-overrides/announcement.php $SSP_PATH/announcement/announcement.php COPY tests /data/tests -RUN cp $SSP_PATH/modules/sildisco/sspoverrides/www_saml2_idp/SSOService.php $SSP_PATH/www/saml2/idp/ +RUN cp $SSP_PATH/modules/sildisco/lib/SSOService.php $SSP_PATH/www/saml2/idp/ RUN chmod a+x /data/run.sh /data/run-tests.sh ADD https://github.com/silinternational/config-shim/releases/latest/download/config-shim.gz config-shim.gz diff --git a/README.md b/README.md index 2fd91468..f920c616 100644 --- a/README.md +++ b/README.md @@ -375,3 +375,29 @@ load balancer) in the TRUSTED_IP_ADDRESSES environment variable (see #### Status Check To check the status of the website, you can access this URL: `https://(your domain name)/module.php/silauth/status.php` + +### SilDisco module for SAML Discovery + +#### Configuration + +Ensure the DYNAMO_* environment variables are set as shown in the local.env.dist file. + +#### Overview + +[Module Overview](./docs/overview.md) + +#### The Hub + +[The Hub](./docs/the_hub.md) + +#### Authprocs + +[Editing Authprocs](./docs/editing_authprocs.md) + +#### Development + +[Development](./docs/development.md) + +#### Functional Testing + +[Functional Testing](./docs/functional_testing.md) diff --git a/actions-services.yml b/actions-services.yml index 756fd699..e3b6b5df 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -16,7 +16,10 @@ services: - ssp-hub.local - ssp-idp1.local - ssp-idp2.local + - ssp-idp3.local - ssp-sp1.local + - ssp-sp2.local + - ssp-sp3.local - pwmanager.local - test-browser environment: @@ -59,7 +62,6 @@ services: # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh - command: /data/run.sh environment: ADMIN_EMAIL: "john_doe@there.com" ADMIN_PASS: "abc123" @@ -88,11 +90,8 @@ services: - ./development/idp-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php - # Misc. files needed - - ./development/enable-exampleauth-module.sh:/data/enable-exampleauth-module.sh - # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code - - ./development/idp-local/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -101,7 +100,6 @@ services: - ./features:/data/features command: > bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && - /data/enable-exampleauth-module.sh && /data/run.sh" environment: ADMIN_EMAIL: "john_doe@there.com" @@ -113,9 +111,9 @@ services: ID_BROKER_ASSERT_VALID_IP: "false" ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" - MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub-custom-port" REMEMBER_ME_SECRET: "12345" - PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub-custom-port" PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" @@ -140,12 +138,9 @@ services: - ./development/idp2-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp2-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php - # Local modules - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - command: /data/run.sh - ports: - - "8086:80" + # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + environment: ADMIN_EMAIL: "john_doe@there.com" ADMIN_PASS: "b" @@ -155,6 +150,28 @@ services: SHOW_SAML_ERRORS: "true" THEME_USE: "material:material" + ssp-idp3.local: + build: . + volumes: + # Utilize custom certs + - ./development/idp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/idp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + - ./development/idp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + + # Utilize custom metadata + - ./development/idp3-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php + - ./development/idp3-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + + environment: + ADMIN_EMAIL: "john_doe@there.com" + ADMIN_PASS: "c" + SECRET_SALT: "h57fjem34fh*nsJFGNjweJ" + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + IDP_NAME: "IdP3" + ssp-sp1.local: build: . volumes: @@ -179,6 +196,51 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" + ssp-sp2.local: + build: . + volumes: + # Utilize custom certs + - ./development/sp2-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp2-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + - ./development/sp2-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp2-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp2 + - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz2 + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + + ssp-sp3.local: + build: . + volumes: + # Utilize custom certs + - ./development/sp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + - ./development/sp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp3-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp3 + - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz3 + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + + pwmanager.local: image: silintl/ssp-base:develop volumes: @@ -204,8 +266,6 @@ services: # the broker and brokerDb containers are used by the silauth module broker: image: silintl/idp-id-broker:latest - ports: - - "80" depends_on: - brokerDb environment: @@ -235,8 +295,6 @@ services: brokerDb: image: mariadb:10 - ports: - - "3306" environment: MYSQL_ROOT_PASSWORD: "r00tp@ss!" MYSQL_DATABASE: "broker" diff --git a/behat.yml b/behat.yml index e05a2b46..6306126b 100644 --- a/behat.yml +++ b/behat.yml @@ -18,6 +18,16 @@ default: profilereview_features: paths: [ '%paths.base%//features//profilereview.feature' ] contexts: [ 'ProfileReviewContext' ] + sildisco_features: + contexts: ['SilDiscoContext'] + paths: + - '%paths.base%//features//Sp1Idp1Sp2Idp2Sp3.feature' + - '%paths.base%//features//Sp1Idp2Sp2Sp3Idp1.feature' + - '%paths.base%//features//Sp2Idp2Sp1Idp1Sp3.feature' + - '%paths.base%//features//Sp2Idp2Sp1Idp2Sp3.feature' + - '%paths.base%//features//Sp3Idp1Sp1Idp1Sp2Idp2.feature' + - '%paths.base%//features//WwwMetadataCept.feature' + - '%paths.base%//features//ZSp1Idp1BetaSp1Idp3.feature' status_features: paths: [ '%paths.base%//features//status.feature' ] contexts: [ 'StatusContext' ] diff --git a/development/idp-local/UserPass.php b/development/UserPass.php similarity index 100% rename from development/idp-local/UserPass.php rename to development/UserPass.php diff --git a/development/enable-exampleauth-module.sh b/development/enable-exampleauth-module.sh deleted file mode 100755 index 5e60e1f7..00000000 --- a/development/enable-exampleauth-module.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -mkdir -p /data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth -touch /data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/enable diff --git a/development/hub/metadata/idp-remote.php b/development/hub/metadata/idp-remote.php index 2b324bf4..56ecd8d6 100644 --- a/development/hub/metadata/idp-remote.php +++ b/development/hub/metadata/idp-remote.php @@ -8,13 +8,13 @@ */ return [ /* - * Guest IdP. Sign in with an "a" (lower case) as the password + * IdP 1 */ 'http://ssp-idp1.local:8085' => [ 'metadata-set' => 'saml20-idp-remote', 'entityid' => 'http://ssp-idp1.local:8085', 'name' => [ - 'en' => 'IDP 1', + 'en' => 'IDP 1:8085', ], 'IDPNamespace' => 'IDP-1-custom-port', 'logoCaption' => 'IDP-1:8085 staff', @@ -26,6 +26,10 @@ 'SingleSignOnService' => 'http://ssp-idp1.local:8085/saml2/idp/SSOService.php', 'SingleLogoutService' => 'http://ssp-idp1.local:8085/saml2/idp/SingleLogoutService.php', 'certData' => 'MIIDzzCCAregAwIBAgIJAPlZYTAQSIbHMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTQ1WhcNMjYxMDE3MTIzMTQ1WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArssOaeKbdOQFpN6bBolwSJ/6QFBXA73Sotg60anx9v6aYdUTmi+b7SVtvOmHDgsD5X8pN/6Z11QCZfTYg2nW3ZevGZsj8W/R6C8lRLHzWUr7e7DXKfj8GKZptHlUs68kn0ndNVt9r/+irJe9KBdZ+4kAihykomNdeZg06bvkklxVcvpkOfLTQzEqJAmISPPIeOXes6hXORdqLuRNTuIKarcZ9rstLnpgAs2TE4XDOrSuUg3XFnM05eDpFQpUb0RXWcD16mLCPWw+CPrGoCfoftD5ZGfll+W2wZ7d0kQ4TbCpNyxQH35q65RPVyVNPgSNSsFFkmdcqP9DsFqjJ8YC6wIDAQABo1AwTjAdBgNVHQ4EFgQUD6oyJKOPPhvLQpDCC3027QcuQwUwHwYDVR0jBBgwFoAUD6oyJKOPPhvLQpDCC3027QcuQwUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAA6tCLHJQGfXGdFerQ3J0wUu8YDSLb0WJqPtGdIuyeiywR5ooJf8G/jjYMPgZArepLQSSi6t8/cjEdkYWejGnjMG323drQ9M1sKMUhOJF4po9R3t7IyvGAL3fSqjXA8JXH5MuGuGtChWxaqhduA0dBJhFAtAXQ61IuIQF7vSFxhTwCvJnaWdWD49sG5OqjCfgIQdY/mw70e45rLnR/bpfoigL67sTJxy+Kx2ogbvMR6lITByOEQFMt7BYpMtXrwvKUM7k9NOo1jREmJacC8PTx//jRhCWwzUj1RsfIri24BuITrawwqMsYl8DZiiwMpjUf9m4NPaf4E7+QRpzo+MCcg==', + + // NOTE: This breaks being able to test the hub's authentication sources + // since the hub doesn't create an SP entry in the session + 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local', 'http://ssp-sp3.local'], ], 'http://ssp-idp1.local' => [ 'metadata-set' => 'saml20-idp-remote', @@ -44,16 +48,20 @@ 'SingleLogoutService' => 'http://ssp-idp1.local/saml2/idp/SingleLogoutService.php', // 'certFingerprint' => 'c9ed4dfb07caf13fc21e0fec1572047eb8a7a4cb' 'certData' => 'MIIDzzCCAregAwIBAgIJAPlZYTAQSIbHMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTQ1WhcNMjYxMDE3MTIzMTQ1WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArssOaeKbdOQFpN6bBolwSJ/6QFBXA73Sotg60anx9v6aYdUTmi+b7SVtvOmHDgsD5X8pN/6Z11QCZfTYg2nW3ZevGZsj8W/R6C8lRLHzWUr7e7DXKfj8GKZptHlUs68kn0ndNVt9r/+irJe9KBdZ+4kAihykomNdeZg06bvkklxVcvpkOfLTQzEqJAmISPPIeOXes6hXORdqLuRNTuIKarcZ9rstLnpgAs2TE4XDOrSuUg3XFnM05eDpFQpUb0RXWcD16mLCPWw+CPrGoCfoftD5ZGfll+W2wZ7d0kQ4TbCpNyxQH35q65RPVyVNPgSNSsFFkmdcqP9DsFqjJ8YC6wIDAQABo1AwTjAdBgNVHQ4EFgQUD6oyJKOPPhvLQpDCC3027QcuQwUwHwYDVR0jBBgwFoAUD6oyJKOPPhvLQpDCC3027QcuQwUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAA6tCLHJQGfXGdFerQ3J0wUu8YDSLb0WJqPtGdIuyeiywR5ooJf8G/jjYMPgZArepLQSSi6t8/cjEdkYWejGnjMG323drQ9M1sKMUhOJF4po9R3t7IyvGAL3fSqjXA8JXH5MuGuGtChWxaqhduA0dBJhFAtAXQ61IuIQF7vSFxhTwCvJnaWdWD49sG5OqjCfgIQdY/mw70e45rLnR/bpfoigL67sTJxy+Kx2ogbvMR6lITByOEQFMt7BYpMtXrwvKUM7k9NOo1jREmJacC8PTx//jRhCWwzUj1RsfIri24BuITrawwqMsYl8DZiiwMpjUf9m4NPaf4E7+QRpzo+MCcg==', + + // NOTE: This breaks being able to test the hub's authentication sources + // since the hub doesn't create an SP entry in the session + 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local', 'http://ssp-sp3.local'], ], /* - * IdP2. Sign in with a "b" (lower case) as the password + * IdP 2 */ 'http://ssp-idp2.local:8086' => [ 'metadata-set' => 'saml20-idp-remote', 'entityid' => 'http://ssp-idp2.local:8086', 'name' => [ - 'en' => 'IDP 2', + 'en' => 'IDP 2:8086', ], 'IDPNamespace' => 'IDP-2-custom-port', 'logoCaption' => 'IDP-2:8086 staff', @@ -66,6 +74,9 @@ 'SingleSignOnService' => 'http://ssp-idp2.local:8086/saml2/idp/SSOService.php', 'SingleLogoutService' => 'http://ssp-idp2.local:8086/saml2/idp/SingleLogoutService.php', 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', + + // limit which Sps can use this IdP + 'SPList' => ['http://ssp-sp1.local:8081', 'http://ssp-sp2.local:8082'], ], 'http://ssp-idp2.local' => [ 'metadata-set' => 'saml20-idp-remote', @@ -84,5 +95,49 @@ 'SingleSignOnService' => 'http://ssp-idp2.local/saml2/idp/SSOService.php', 'SingleLogoutService' => 'http://ssp-idp2.local/saml2/idp/SingleLogoutService.php', 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', + + // limit which Sps can use this IdP + 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local'], + ], + + /* + * IdP 3 + */ + 'http://ssp-idp3.local:8087' => [ + 'metadata-set' => 'saml20-idp-remote', + 'entityid' => 'http://ssp-idp3.local:8087', + 'name' => [ + 'en' => 'IDP 3:8087', + ], + 'IDPNamespace' => 'IDP-3-custom-port', + 'logoCaption' => 'IDP-3:8087 staff', + 'enabled' => false, + 'betaEnabled' => true, + 'logoURL' => 'https://dummyimage.com/125x125/0f4fbd/ffffff.png&text=IDP+3+8087', + + 'description' => 'Local IDP3 for testing SSP Hub (custom port)', + + 'SingleSignOnService' => 'http://ssp-idp3.local:8087/saml2/idp/SSOService.php', + 'SingleLogoutService' => 'http://ssp-idp3.local:8087/saml2/idp/SingleLogoutService.php', + 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', ], + 'http://ssp-idp3.local' => [ + 'metadata-set' => 'saml20-idp-remote', + 'entityid' => 'http://ssp-idp3.local', + 'name' => [ + 'en' => 'IDP 3', + ], + 'IDPNamespace' => 'IDP-3', + 'logoCaption' => 'IDP-3 staff', + 'enabled' => false, + 'betaEnabled' => true, + 'logoURL' => 'https://dummyimage.com/125x125/0f4fbd/ffffff.png&text=IDP+3', + + 'description' => 'Local IDP3 for testing SSP Hub', + + 'SingleSignOnService' => 'http://ssp-idp3.local/saml2/idp/SSOService.php', + 'SingleLogoutService' => 'http://ssp-idp3.local/saml2/idp/SingleLogoutService.php', + 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', + ], + ]; diff --git a/development/hub/metadata/sp-remote.php b/development/hub/metadata/sp-remote.php index 988ebec7..68f6974e 100644 --- a/development/hub/metadata/sp-remote.php +++ b/development/hub/metadata/sp-remote.php @@ -10,26 +10,28 @@ * Example SimpleSAMLphp SAML 2.0 SP */ 'http://ssp-sp1.local:8081' => [ + 'name' => "SP1 (custom port)", + 'AssertionConsumerService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-acs.php/ssp-hub-custom-port', + 'SingleLogoutService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-logout.php/ssp-hub-custom-port', + 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'IDPList' => [ 'http://ssp-idp1.local:8085', 'http://ssp-idp2.local:8086', + 'http://ssp-idp3.local:8087', ], - 'name' => "SP Local", - 'AssertionConsumerService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-acs.php/ssp-hub-custom-port', - 'SingleLogoutService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-logout.php/ssp-hub-custom-port', - 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'assertion.encryption' => true, ], 'http://ssp-sp1.local' => [ + 'name' => "SP1", + 'AssertionConsumerService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-acs.php/ssp-hub', + 'SingleLogoutService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-logout.php/ssp-hub', + 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'IDPList' => [ 'http://ssp-idp1.local', 'http://ssp-idp2.local', + 'http://ssp-idp3.local', ], - 'name' => "SP Local", - 'AssertionConsumerService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-acs.php/ssp-hub', - 'SingleLogoutService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-logout.php/ssp-hub', - 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'assertion.encryption' => true, ], @@ -39,7 +41,7 @@ 'IDPList' => [ 'http://ssp-idp2.local:8086', ], - 'name' => 'SP 2 (custom port)', + 'name' => 'SP2 (custom port)', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'assertion.encryption' => true, ], @@ -50,7 +52,35 @@ 'IDPList' => [ 'http://ssp-idp2.local', ], - 'name' => 'SP 2', + 'name' => 'SP2', + 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', + 'assertion.encryption' => true, + ], + + // for test purposes, SP3 should be on the SPList entry of idp2 + + 'http://ssp-sp3.local:8083' => [ + 'AssertionConsumerService' => 'http://ssp-sp3.local:8083/module.php/saml/sp/saml2-acs.php/ssp-hub', + 'SingleLogoutService' => 'http://ssp-sp3.local:8083/module.php/saml/sp/saml2-logout.php/ssp-hub', + 'IDPList' => [ + 'http://ssp-idp1.local:8085', + 'http://ssp-idp2.local:8086', // overruled by Idp2 + 'http://ssp-idp3.local:8087' + ], + 'name' => 'SP3 (custom port)', + 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', + 'assertion.encryption' => true, + ], + + 'http://ssp-sp3.local' => [ + 'AssertionConsumerService' => 'http://ssp-sp3.local/module.php/saml/sp/saml2-acs.php/ssp-hub', + 'SingleLogoutService' => 'http://ssp-sp3.local/module.php/saml/sp/saml2-logout.php/ssp-hub', + 'IDPList' => [ + 'http://ssp-idp1.local', + 'http://ssp-idp2.local', // overruled by Idp2 + 'http://ssp-idp3.local' + ], + 'name' => 'SP3', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'assertion.encryption' => true, ], diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 28c8124a..b03e4fcb 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -1186,5 +1186,29 @@ ], 'manager_email' => ['manager@example.com'], ], + + // sildisco test user + 'sildisco_idp1:sildisco_password' => [ + 'eduPersonPrincipalName' => ['sildisco@idp1'], + 'eduPersonTargetID' => ['57de1930-c5d2-4f6f-9318-d85a939c45d8'], + 'sn' => ['IDP1'], + 'givenName' => ['SilDisco'], + 'mail' => ['sildisco_idp1@example.com'], + 'employeeNumber' => ['50001'], + 'cn' => ['SILDISCO_IDP1'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'no', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + ], + 'profile_review' => 'no' + ], + ], ]; diff --git a/development/idp-local/config/config.php b/development/idp-local/config/config.php index 685f66f1..db650c58 100644 --- a/development/idp-local/config/config.php +++ b/development/idp-local/config/config.php @@ -599,6 +599,7 @@ 'profilereview' => true, 'silauth' => true, 'sildisco' => true, + 'exampleauth' => true, ], diff --git a/development/idp2-local/config/authsources.php b/development/idp2-local/config/authsources.php index a5d7d017..197f61b1 100644 --- a/development/idp2-local/config/authsources.php +++ b/development/idp2-local/config/authsources.php @@ -10,4 +10,30 @@ 'core:AdminPassword', ], + 'example-userpass' => [ + 'exampleauth:UserPass', + + // sildisco test user + 'sildisco_idp2:sildisco_password' => [ + 'eduPersonPrincipalName' => ['sildisco@idp2'], + 'eduPersonTargetID' => ['57de2930-c5d2-4f6f-9328-d85a939c45d8'], + 'sn' => ['IDP2'], + 'givenName' => ['SilDisco'], + 'mail' => ['sildisco_idp2@example.com'], + 'employeeNumber' => ['50002'], + 'cn' => ['SILDISCO_IDP2'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'no', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + ], + 'profile_review' => 'no' + ], + ] ]; diff --git a/development/idp2-local/config/config.php b/development/idp2-local/config/config.php index 685f66f1..db650c58 100644 --- a/development/idp2-local/config/config.php +++ b/development/idp2-local/config/config.php @@ -599,6 +599,7 @@ 'profilereview' => true, 'silauth' => true, 'sildisco' => true, + 'exampleauth' => true, ], diff --git a/development/idp2-local/metadata/saml20-idp-hosted.php b/development/idp2-local/metadata/saml20-idp-hosted.php index 78ff4405..ad7b8705 100644 --- a/development/idp2-local/metadata/saml20-idp-hosted.php +++ b/development/idp2-local/metadata/saml20-idp-hosted.php @@ -21,7 +21,7 @@ * Authentication source to use. Must be one that is configured in * 'config/authsources.php'. */ - 'auth' => 'admin', + 'auth' => 'example-userpass', ]; // Copy configuration for port 80 and modify host. diff --git a/development/idp3-local/cert/ssp-hub-idp2.crt b/development/idp3-local/cert/ssp-hub-idp2.crt new file mode 100644 index 00000000..bcbf054d --- /dev/null +++ b/development/idp3-local/cert/ssp-hub-idp2.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANT +SUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkB +FhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4 +MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldh +eGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2 +ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iL +X/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB ++9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvn +H1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0 +bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSB +OsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1Aw +TjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxl +UQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK9 +9RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuE +ho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR +1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmo +C737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo +240Rh+pDLwxdxPqRAZdeLaUkCQ== +-----END CERTIFICATE----- diff --git a/development/idp3-local/cert/ssp-hub-idp2.pem b/development/idp3-local/cert/ssp-hub-idp2.pem new file mode 100644 index 00000000..7674ef99 --- /dev/null +++ b/development/idp3-local/cert/ssp-hub-idp2.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHmZk3CMSdqQmG +j7l624XLH7aD2uz3qItf8sMLbhoECx03FM24hzWMnunEHEat6kQ0PguSJ60s0GU6 +vWrnSerb511OsJ+agkH70GoE7Fyb2pZ6pZunz9qtSNrE/MrcN9mvEnEhUWPOoUXu +Aj3XL3K8Rb9+otEyG+cfWez43Wd7n3OvXLqH2aYd8F98avVhcB/IQC5XWJyHgB9N ++OqF3AXDGEUssJWfEDRsNs3D/R4YJKIDXc3trTXfFO6st4m7QBBpliywyq9z/XtS +mEfiBYMwRkDDLQR9FIE6wM5gLF43FVqOdZfXzfhUOkEN+mdmP33oaHy3NFDFCBP3 ++mUcqEaLAgMBAAECggEACinHBGdc44483u4oipns6RfXSkV2dXHOjvckeUuE5ZnP +RgO4KeIwltVsn8C01JwuFt7l5e5BQhvmW6RTci1wWPwh4yTZK5vgUjsdetyyJnlt +2hbeox/RSauBADDC/42Udvagbgrf4yCRF/pjPba7x9xhUMhnkH6dORpyF4XmhAPW +TVCA7VVRL5aoEfemiZYOpjPkY135QqI6/PaLbRDUkqUtKdAB2+/XRTF2K8gbb44x +f/wZeqpOG1y82P3aYVM1f3RLQUAS0rKyQJBRB8fHy5LY2z9LAlC8KSp1BAIKtqMT +lUr6MIs2oImrLL0JyvEbcmtZI4MdGgnmkxrjc/8ZYQKBgQD8t18HVfmTu+5HZCuv +NItpLOu/uxm6UwwAwbljtM2K2562wCsu9/tt72V0Ismysz19VUva/FtSqksuOWcA +HC+APHtWMMtsBcQMZGrFHUlJCKv963gu7CoeJvY3mSWm4t8xuZBSz5pAeoeENioH +NrL4+K2+cmVGRNjKIDipN5Ng8QKBgQDKMYrqNmH8/IaIDTi27d1A+1YTEZe/toaP +YbTyyQ731mLwnukAx1MhFgoXe294nXiD3tC0g6ISpFgyUTL5OplIs/yiXks0y5/G +mKxGsVc5qtBQB8utA3i8EzT6x2fIYmuJY2Pj3r6jFFzqOjlILN8ct1v05qjKH+gM +n5C/IC/fOwKBgQCEcubPRXQkxZ5AtHNgxD08xlpYhosZaGUmEGJFq4D+gdRRG66G +U1nnaEzX7VOg4OgdRBMZlqGWVcJJW7RsDlmm8AwERFaZKvxxMj/zR0IdkPnzfvHi +RcxdOTZaNV3SdZ1cxlCp1jyWBqH33Rtx5G0wp8UHx5Tkmziz1udbaNFJQQKBgQDA +EvpE7i59tqJSQkUbObFSVrB44uCGJW2EbawIa0lF1KoerMbpj3B/4MDrd734FZdz +pkobAWUIUojaG9rReYI914Vp9St6VulMLqKRcUxMIuFK9WzdyYt7Fr/gb2c+q4g+ +dmVhBauRnfl6JJ9f2giE7gZ0Cl5TzKWSwE4v0fLIGwKBgGuiI+2j8YOsV4LYyin+ +9p5qmk4gVUe5ohPUKCdPeaZiiQbAJ3l5B3LR2sgV1mOm996Nm9Y0HEback4ISAjz +Nd3TkcwDVaa7GV9pMknM2rK0U6gupbtPAaTMCanXu2VZbKGfQDlkpE3iYvMsuGIW +1ppvkZ+ZtqGlvPGk+CWjr6vu +-----END PRIVATE KEY----- diff --git a/development/idp3-local/config/authsources.php b/development/idp3-local/config/authsources.php new file mode 100644 index 00000000..a5d7d017 --- /dev/null +++ b/development/idp3-local/config/authsources.php @@ -0,0 +1,13 @@ + [ + // The default is to use core:AdminPassword, but it can be replaced with + // any authentication source. + + 'core:AdminPassword', + ], + +]; diff --git a/development/idp3-local/config/config.php b/development/idp3-local/config/config.php new file mode 100644 index 00000000..656009fa --- /dev/null +++ b/development/idp3-local/config/config.php @@ -0,0 +1,804 @@ + $BASE_URL_PATH, + 'certdir' => 'cert/', + 'loggingdir' => 'log/', + 'datadir' => 'data/', + + /* + * A directory where simpleSAMLphp can save temporary files. + * + * SimpleSAMLphp will attempt to create this directory if it doesn't exist. + */ + 'tempdir' => '/tmp/simplesaml', + + + /* + * If you enable this option, simpleSAMLphp will log all sent and received messages + * to the log file. + * + * This option also enables logging of the messages that are encrypted and decrypted. + * + * Note: The messages are logged with the DEBUG log level, so you also need to set + * the 'logging.level' option to LOG_DEBUG. + */ + 'debug' => false, + + /* + * When showerrors is enabled, all error messages and stack traces will be output + * to the browser. + * + * When errorreporting is enabled, a form will be presented for the user to report + * the error to technicalcontact_email. + */ + 'showerrors' => $SHOW_SAML_ERRORS, + 'errorreporting' => false, + + /* + * Custom error show function called from \SimpleSAML\Error\Error::show. + * See docs/simplesamlphp-errorhandling.txt for function code example. + * + * Example: + * 'errors.show_function' => array('\SimpleSAML\Module\example\Error\Show', 'show'), + */ + + /* + * This option allows you to enable validation of XML data against its + * schemas. A warning will be written to the log if validation fails. + */ + 'debug.validatexml' => false, + + /* + * This password must be kept secret, and modified from the default value 123. + * This password will give access to the installation page of simpleSAMLphp with + * metadata listing and diagnostics pages. + * You can also put a hash here; run "bin/pwgen.php" to generate one. + */ + 'auth.adminpassword' => $ADMIN_PASS, + 'admin.protectindexpage' => $ADMIN_PROTECT_INDEX_PAGE, + 'admin.protectmetadata' => true, + + /* + * This is a secret salt used by simpleSAMLphp when it needs to generate a secure hash + * of a value. It must be changed from its default value to a secret value. The value of + * 'secretsalt' can be any valid string of any length. + * + * A possible way to generate a random salt is by running the following command from a unix shell: + * tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo + */ + 'secretsalt' => $SECRET_SALT, + + /* + * Some information about the technical persons running this installation. + * The email address will be used as the recipient address for error reports, and + * also as the technical contact in generated metadata. + */ + 'technicalcontact_name' => $ADMIN_NAME, + 'technicalcontact_email' => $ADMIN_EMAIL, + + /* + * The timezone of the server. This option should be set to the timezone you want + * simpleSAMLphp to report the time in. The default is to guess the timezone based + * on your system timezone. + * + * See this page for a list of valid timezones: http://php.net/manual/en/timezones.php + */ + 'timezone' => $TIMEZONE, + + /* + * Logging. + * + * define the minimum log level to log + * \SimpleSAML\Logger::ERR No statistics, only errors + * \SimpleSAML\Logger::WARNING No statistics, only warnings/errors + * \SimpleSAML\Logger::NOTICE Statistics and errors + * \SimpleSAML\Logger::INFO Verbose logs + * \SimpleSAML\Logger::DEBUG Full debug logs - not reccomended for production + * + * Choose logging handler. + * + * Options: [syslog,file,errorlog] + * + */ + 'logging.level' => \SimpleSAML\Logger::NOTICE, + 'logging.handler' => $LOGGING_HANDLER, + + /* + * Specify the format of the logs. Its use varies depending on the log handler used (for instance, you cannot + * control here how dates are displayed when using the syslog or errorlog handlers), but in general the options + * are: + * + * - %date{}: the date and time, with its format specified inside the brackets. See the PHP documentation + * of the strftime() function for more information on the format. If the brackets are omitted, the standard + * format is applied. This can be useful if you just want to control the placement of the date, but don't care + * about the format. + * + * - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname' + * option below. + * + * - %level: the log level (name or number depending on the handler used). + * + * - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind + * the trailing space). + * + * - %trackid: the track ID, an identifier that allows you to track a single session. + * + * - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the + * $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header. + * + * - %msg: the message to be logged. + * + */ + //'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg', + + /* + * Choose which facility should be used when logging with syslog. + * + * These can be used for filtering the syslog output from simpleSAMLphp into its + * own file by configuring the syslog daemon. + * + * See the documentation for openlog (http://php.net/manual/en/function.openlog.php) for available + * facilities. Note that only LOG_USER is valid on windows. + * + * The default is to use LOG_LOCAL5 if available, and fall back to LOG_USER if not. + */ + 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, + + /* + * The process name that should be used when logging to syslog. + * The value is also written out by the other logging handlers. + */ + 'logging.processname' => 'simplesamlphp', + + /* Logging: file - Logfilename in the loggingdir from above. + */ + 'logging.logfile' => 'simplesamlphp.log', + + /* (New) statistics output configuration. + * + * This is an array of outputs. Each output has at least a 'class' option, which + * selects the output. + */ + 'statistics.out' => [// Log statistics to the normal log. + /* + [ + 'class' => 'core:Log', + 'level' => 'notice', + ], + */ + // Log statistics to files in a directory. One file per day. + /* + [ + 'class' => 'core:File', + 'directory' => '/var/log/stats', + ], + */ + ], + + + /* + * Enable + * + * Which functionality in simpleSAMLphp do you want to enable. Normally you would enable only + * one of the functionalities below, but in some cases you could run multiple functionalities. + * In example when you are setting up a federation bridge. + */ + 'enable.saml20-idp' => $SAML20_IDP_ENABLE, + 'enable.shib13-idp' => false, + 'enable.adfs-idp' => false, + 'enable.wsfed-sp' => false, + 'enable.authmemcookie' => false, + + + /* + * Module enable configuration + * + * Configuration to override module enabling/disabling. + * + * Example: + * + * 'module.enable' => array( + * // Setting to TRUE enables. + * 'exampleauth' => true, + * // Setting to FALSE disables. + * 'saml' => false, + * // Unset or NULL uses default. + * 'core' => NULL, + * ), + * + */ + + 'module.enable' => [ + // Setting to TRUE enables. + 'authgoogle' => $GOOGLE_ENABLE, + ], + + /* + * This value is the duration of the session in seconds. Make sure that the time duration of + * cookies both at the SP and the IdP exceeds this duration. + */ + 'session.duration' => $SESSION_DURATION, + + /* + * Sets the duration, in seconds, data should be stored in the datastore. As the datastore is used for + * login and logout requests, thid option will control the maximum time these operations can take. + * The default is 4 hours (4*60*60) seconds, which should be more than enough for these operations. + */ + 'session.datastore.timeout' => $SESSION_DATASTORE_TIMEOUT, + + /* + * Sets the duration, in seconds, auth state should be stored. + */ + 'session.state.timeout' => $SESSION_STATE_TIMEOUT, + + /* + * Option to override the default settings for the session cookie name + */ + 'session.cookie.name' => 'SSPSESSID', + + /* + * Expiration time for the session cookie, in seconds. + * + * Defaults to 0, which means that the cookie expires when the browser is closed. + * + * Example: + * 'session.cookie.lifetime' => 30*60, + */ + 'session.cookie.lifetime' => $SESSION_COOKIE_LIFETIME, + + /* + * Limit the path of the cookies. + * + * Can be used to limit the path of the cookies to a specific subdirectory. + * + * Example: + * 'session.cookie.path' => '/simplesaml/', + */ + 'session.cookie.path' => '/', + + /* + * Cookie domain. + * + * Can be used to make the session cookie available to several domains. + * + * Example: + * 'session.cookie.domain' => '.example.org', + */ + 'session.cookie.domain' => null, + + /* + * Set the secure flag in the cookie. + * + * Set this to TRUE if the user only accesses your service + * through https. If the user can access the service through + * both http and https, this must be set to FALSE. + */ + 'session.cookie.secure' => $SECURE_COOKIE, + + /* + * When set to FALSE fallback to transient session on session initialization + * failure, throw exception otherwise. + */ + 'session.disable_fallback' => false, + + /* + * Enable secure POST from HTTPS to HTTP. + * + * If you have some SP's on HTTP and IdP is normally on HTTPS, this option + * enables secure POSTing to HTTP endpoint without warning from browser. + * + * For this to work, module.php/core/postredirect.php must be accessible + * also via HTTP on IdP, e.g. if your IdP is on + * https://idp.example.org/ssp/, then + * http://idp.example.org/ssp/module.php/core/postredirect.php must be accessible. + */ + 'enable.http_post' => false, + + /* + * Options to override the default settings for php sessions. + */ + 'session.phpsession.cookiename' => null, + 'session.phpsession.savepath' => null, + 'session.phpsession.httponly' => true, + + /* + * Option to override the default settings for the auth token cookie + */ + 'session.authtoken.cookiename' => 'SSPAUTHTOKEN', + + /* + * Options for remember me feature for IdP sessions. Remember me feature + * has to be also implemented in authentication source used. + * + * Option 'session.cookie.lifetime' should be set to zero (0), i.e. cookie + * expires on browser session if remember me is not checked. + * + * Session duration ('session.duration' option) should be set according to + * 'session.rememberme.lifetime' option. + * + * It's advised to use remember me feature with session checking function + * defined with 'session.check_function' option. + */ + 'session.rememberme.enable' => false, + 'session.rememberme.checked' => false, + 'session.rememberme.lifetime' => $SESSION_REMEMBERME_LIFETIME, + + /** + * Custom function for session checking called on session init and loading. + * See docs/simplesamlphp-advancedfeatures.txt for function code example. + * + * Example: + * 'session.check_function' => array('\SimpleSAML\Module\example\Util', 'checkSession'), + */ + + /* + * Languages available, RTL languages, and what language is default + */ + 'language.available' => array( + 'en', 'es', 'fr', 'pt', + ), + 'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'), + 'language.default' => 'en', + + /* + * Options to override the default settings for the language parameter + */ + 'language.parameter.name' => 'language', + 'language.parameter.setcookie' => true, + + /* + * Options to override the default settings for the language cookie + */ + 'language.cookie.name' => 'language', + 'language.cookie.domain' => null, + 'language.cookie.path' => '/', + 'language.cookie.lifetime' => (60 * 60 * 24 * 900), + + /** + * Custom getLanguage function called from \SimpleSAML\XHTML\Template::getLanguage(). + * Function should return language code of one of the available languages or NULL. + * See \SimpleSAML\XHTML\Template::getLanguage() source code for more info. + * + * This option can be used to implement a custom function for determining + * the default language for the user. + * + * Example: + * 'language.get_language_function' => array('\SimpleSAML\Module\example\Template', 'getLanguage'), + */ + + /* + * Extra dictionary for attribute names. + * This can be used to define local attributes. + * + * The format of the parameter is a string with :. + * + * Specifying this option will cause us to look for modules//dictionaries/.definition.json + * The dictionary should look something like: + * + * { + * "firstattribute": { + * "en": "English name", + * "no": "Norwegian name" + * }, + * "secondattribute": { + * "en": "English name", + * "no": "Norwegian name" + * } + * } + * + * Note that all attribute names in the dictionary must in lowercase. + * + * Example: 'attributes.extradictionary' => 'ourmodule:ourattributes', + */ + 'attributes.extradictionary' => null, + + /* + * Which theme directory should be used? + */ + 'theme.use' => $THEME_USE, + + + /* + * Default IdP for WS-Fed. + */ + // 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost', + + /* + * Whether the discovery service should allow the user to save his choice of IdP. + */ + 'idpdisco.enableremember' => true, + 'idpdisco.rememberchecked' => true, + + // Disco service only accepts entities it knows. + 'idpdisco.validate' => true, + + 'idpdisco.extDiscoveryStorage' => null, + + /* + * IdP Discovery service look configuration. + * Wether to display a list of idp or to display a dropdown box. For many IdP' a dropdown box + * gives the best use experience. + * + * When using dropdown box a cookie is used to highlight the previously chosen IdP in the dropdown. + * This makes it easier for the user to choose the IdP + * + * Options: [links,dropdown] + * + */ + 'idpdisco.layout' => 'links', + + /* + * Whether simpleSAMLphp should sign the response or the assertion in SAML 1.1 authentication + * responses. + * + * The default is to sign the assertion element, but that can be overridden by setting this + * option to TRUE. It can also be overridden on a pr. SP basis by adding an option with the + * same name to the metadata of the SP. + */ + 'shib13.signresponse' => true, + + + /* + * Authentication processing filters that will be executed for all IdPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.idp' => [ + /* Enable the authproc filter below to add URN Prefixces to all attributes + 10 => array( + 'class' => 'core:AttributeMap', 'addurnprefix' + ), */ + /* Enable the authproc filter below to automatically generated eduPersonTargetedID. + 20 => 'core:TargetedID', + */ + + // Adopts language from attribute to use in UI + 30 => 'core:LanguageAdaptor', + + /* Add a realm attribute from edupersonprincipalname + 40 => 'core:AttributeRealm', + */ + 45 => [ + 'class' => 'core:StatisticsWithAttribute', + 'attributename' => 'realm', + 'type' => 'saml20-idp-SSO', + ], + + // Add one to help with testing + 50 => [ + 'class' => 'core:AttributeAdd', + 'eduPersonPrincipalName' => 'TEST_ADMIN', + 'urn:oid:0.9.2342.19200300.100.1.3' => 'test_admin@idp3.org', + 'uid' => '333366', + ], + + // Use the uid value to populate the nameid entry + 60 => [ + 'class' => 'saml:AttributeNameID', + 'attribute' => 'uid', + 'Format' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + ], + + /* + * Search attribute "distinguishedName" for pattern and replaces if found + + 70 => array( + 'class' => 'core:AttributeAlter', + 'pattern' => '/OU=studerende/', + 'replacement' => 'Student', + 'subject' => 'distinguishedName', + '%replace', + ), + */ + + /* + * Consent module is enabled (with no permanent storage, using cookies). + + 90 => array( + 'class' => 'consent:Consent', + 'store' => 'consent:Cookie', + 'focus' => 'yes', + 'checked' => true + ), + */ + + + // If language is set in Consent module it will be added as an attribute. + 99 => 'core:LanguageAdaptor', + ], + /* + * Authentication processing filters that will be executed for all SPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.sp' => [ + /* + 10 => array( + 'class' => 'core:AttributeMap', 'removeurnprefix' + ), + */ + + /* + * Generate the 'group' attribute populated from other variables, including eduPersonAffiliation. + 60 => array( + 'class' => 'core:GenerateGroups', 'eduPersonAffiliation' + ), + */ + /* + * All users will be members of 'users' and 'members' + 61 => array( + 'class' => 'core:AttributeAdd', 'groups' => array('users', 'members') + ), + */ + + // Adopts language from attribute to use in UI + 90 => 'core:LanguageAdaptor', + + ], + + + /* + * This option configures the metadata sources. The metadata sources is given as an array with + * different metadata sources. When searching for metadata, simpleSAMPphp will search through + * the array from start to end. + * + * Each element in the array is an associative array which configures the metadata source. + * The type of the metadata source is given by the 'type' element. For each type we have + * different configuration options. + * + * Flat file metadata handler: + * - 'type': This is always 'flatfile'. + * - 'directory': The directory we will load the metadata files from. The default value for + * this option is the value of the 'metadatadir' configuration option, or + * 'metadata/' if that option is unset. + * + * XML metadata handler: + * This metadata handler parses an XML file with either an EntityDescriptor element or an + * EntitiesDescriptor element. The XML file may be stored locally, or (for debugging) on a remote + * web server. + * The XML hetadata handler defines the following options: + * - 'type': This is always 'xml'. + * - 'file': Path to the XML file with the metadata. + * - 'url': The URL to fetch metadata from. THIS IS ONLY FOR DEBUGGING - THERE IS NO CACHING OF THE RESPONSE. + * + * + * Examples: + * + * This example defines two flatfile sources. One is the default metadata directory, the other + * is a metadata directory with autogenerated metadata files. + * + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'flatfile', 'directory' => 'metadata-generated'), + * ), + * + * This example defines a flatfile source and an XML source. + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'xml', 'file' => 'idp.example.org-idpMeta.xml'), + * ), + * + * + * Default: + * 'metadata.sources' => array( + * array('type' => 'flatfile') + * ), + */ + 'metadata.sources' => [ + ['type' => 'flatfile'], + ], + + + /* + * Configure the datastore for simpleSAMLphp. + * + * - 'phpsession': Limited datastore, which uses the PHP session. + * - 'memcache': Key-value datastore, based on memcache. + * - 'sql': SQL datastore, using PDO. + * + * The default datastore is 'phpsession'. + * + * (This option replaces the old 'session.handler'-option.) + */ + 'store.type' => 'phpsession', + + + /* + * The DSN the sql datastore should connect to. + * + * See http://www.php.net/manual/en/pdo.drivers.php for the various + * syntaxes. + */ + 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3', + + /* + * The username and password to use when connecting to the database. + */ + 'store.sql.username' => null, + 'store.sql.password' => null, + + /* + * The prefix we should use on our tables. + */ + 'store.sql.prefix' => 'simpleSAMLphp', + + + /* + * Configuration for the MemcacheStore class. This allows you to store + * multiple redudant copies of sessions on different memcache servers. + * + * 'memcache_store.servers' is an array of server groups. Every data + * item will be mirrored in every server group. + * + * Each server group is an array of servers. The data items will be + * load-balanced between all servers in each server group. + * + * Each server is an array of parameters for the server. The following + * options are available: + * - 'hostname': This is the hostname or ip address where the + * memcache server runs. This is the only required option. + * - 'port': This is the port number of the memcache server. If this + * option isn't set, then we will use the 'memcache.default_port' + * ini setting. This is 11211 by default. + * - 'weight': This sets the weight of this server in this server + * group. http://php.net/manual/en/function.Memcache-addServer.php + * contains more information about the weight option. + * - 'timeout': The timeout for this server. By default, the timeout + * is 3 seconds. + * + * Example of redudant configuration with load balancing: + * This configuration makes it possible to lose both servers in the + * a-group or both servers in the b-group without losing any sessions. + * Note that sessions will be lost if one server is lost from both the + * a-group and the b-group. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'mc_a1'), + * array('hostname' => 'mc_a2'), + * ), + * array( + * array('hostname' => 'mc_b1'), + * array('hostname' => 'mc_b2'), + * ), + * ), + * + * Example of simple configuration with only one memcache server, + * running on the same computer as the web server: + * Note that all sessions will be lost if the memcache server crashes. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'localhost'), + * ), + * ), + * + */ + 'memcache_store.servers' => [ + [ + ['hostname' => 'localhost'], + ], + ], + + + /* + * This value is the duration data should be stored in memcache. Data + * will be dropped from the memcache servers when this time expires. + * The time will be reset every time the data is written to the + * memcache servers. + * + * This value should always be larger than the 'session.duration' + * option. Not doing this may result in the session being deleted from + * the memcache servers while it is still in use. + * + * Set this value to 0 if you don't want data to expire. + * + * Note: The oldest data will always be deleted if the memcache server + * runs out of storage space. + */ + 'memcache_store.expires' => 36 * (60 * 60), // 36 hours. + + + /* + * Should signing of generated metadata be enabled by default. + * + * Metadata signing can also be enabled for a individual SP or IdP by setting the + * same option in the metadata for the SP or IdP. + */ + 'metadata.sign.enable' => true, + + /* + * The default key & certificate which should be used to sign generated metadata. These + * are files stored in the cert dir. + * These values can be overridden by the options with the same names in the SP or + * IdP metadata. + * + * If these aren't specified here or in the metadata for the SP or IdP, then + * the 'certificate' and 'privatekey' option in the metadata will be used. + * if those aren't set, signing of metadata will fail. + */ + 'metadata.sign.privatekey' => 'ssp-hub.pem', + 'metadata.sign.privatekey_pass' => null, + 'metadata.sign.certificate' => 'ssp-hub.crt', + + + /* + * Proxy to use for retrieving URLs. + * + * Example: + * 'proxy' => 'tcp://proxy.example.com:5100' + */ + 'proxy' => null, + + /* + * Array of domains that are allowed when generating links or redirections + * to URLs. simpleSAMLphp will use this option to determine whether to + * to consider a given URL valid or not, but you should always validate + * URLs obtained from the input on your own (i.e. ReturnTo or RelayState + * parameters obtained from the $_REQUEST array). + * + * Set to NULL to disable checking of URLs. + * + * simpleSAMLphp will automatically add your own domain (either by checking + * it dinamically, or by using the domain defined in the 'baseurlpath' + * directive, the latter having precedence) to the list of trusted domains, + * in case this option is NOT set to NULL. In that case, you are explicitly + * telling simpleSAMLphp to verify URLs. + * + * Set to an empty array to disallow ALL redirections or links pointing to + * an external URL other than your own domain. + * + * Example: + * 'trusted.url.domains' => array('sp.example.com', 'app.example.com'), + */ + 'trusted.url.domains' => null, + +]; diff --git a/development/idp3-local/metadata/saml20-idp-hosted.php b/development/idp3-local/metadata/saml20-idp-hosted.php new file mode 100644 index 00000000..37d487af --- /dev/null +++ b/development/idp3-local/metadata/saml20-idp-hosted.php @@ -0,0 +1,25 @@ + '__DEFAULT__', + + // X.509 key and certificate. Relative to the cert directory. + 'privatekey' => 'ssp-hub-idp2.pem', + 'certificate' => 'ssp-hub-idp2.crt', + + /* + * Authentication source to use. Must be one that is configured in + * 'config/authsources.php'. + */ + 'auth' => 'admin', +]; diff --git a/development/idp3-local/metadata/saml20-sp-remote.php b/development/idp3-local/metadata/saml20-sp-remote.php new file mode 100644 index 00000000..f8c12aae --- /dev/null +++ b/development/idp3-local/metadata/saml20-sp-remote.php @@ -0,0 +1,26 @@ + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'AssertionConsumerService' => 'http://ssp-hub.local/module.php/sildisco/sp/saml2-acs.php/hub-discovery', + 'SingleLogoutService' => 'http://ssp-hub.local/module.php/sildisco/sp/saml2-logout.php/hub-discovery', + 'certData' => 'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', +]; + +/* + * IdP Hub for automated tests + */ +$metadata['hub4tests'] = array( + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'AssertionConsumerService' => 'http://hub4tests/module.php/sildisco/sp/saml2-acs.php/hub-discovery', + 'SingleLogoutService' => 'http://hub4tests/module.php/sildisco/sp/saml2-logout.php/hub-discovery', + 'certData' => 'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', +); \ No newline at end of file diff --git a/development/init-dynamodb.sh b/development/init-dynamodb.sh new file mode 100755 index 00000000..f944c583 --- /dev/null +++ b/development/init-dynamodb.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env ash + +# Create data table +aws dynamodb create-table --table-name sildisco_local_user-log \ + --attribute-definitions AttributeName=ID,AttributeType=S \ + --key-schema AttributeName=ID,KeyType=HASH \ + --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10 \ + --endpoint-url http://dynamo:8000 + + +# Enable Time to Live +aws dynamodb update-time-to-live --table-name sildisco_local_user-log \ + --time-to-live-specification "Enabled=true,AttributeName=ExpiresAt" \ + --endpoint-url http://dynamo:8000 \ No newline at end of file diff --git a/development/sp-local/config/authsources-pwmanager.php b/development/sp-local/config/authsources-pwmanager.php index ea9c8ab0..80aee9c9 100644 --- a/development/sp-local/config/authsources-pwmanager.php +++ b/development/sp-local/config/authsources-pwmanager.php @@ -12,7 +12,7 @@ 'mfa-idp' => [ 'saml:SP', - 'entityID' => 'http://pwmanager.local:8083', + 'entityID' => 'http://pwmanager.local:8084', 'idp' => 'http://ssp-idp1.local:8085', 'discoURL' => null, 'NameIDPolicy' => "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", diff --git a/development/sp2-local/config/authsources.php b/development/sp2-local/config/authsources.php index b48c271c..60f76215 100644 --- a/development/sp2-local/config/authsources.php +++ b/development/sp2-local/config/authsources.php @@ -27,6 +27,9 @@ // The URL to the discovery service. // Can be NULL/unset, in which case a builtin discovery service will be used. 'discoURL' => null, + + // Specify what private key to use (such as for decrypting assertions). + 'privatekey' => 'ssp-hub-sp2.pem', ], 'ssp-hub-custom-port' => [ diff --git a/development/sp3-local/cert/ssp-hub-sp3.crt b/development/sp3-local/cert/ssp-hub-sp3.crt new file mode 100644 index 00000000..21ed1c2b --- /dev/null +++ b/development/sp3-local/cert/ssp-hub-sp3.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANT +SUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkB +FhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3 +MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldh +eGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2 +ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYos +gyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/c +esppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoK +GjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBg +dk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I +47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1Aw +TjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYc +MbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+X +rgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVw +PUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298Hu +vsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejt +FBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkh +T0DHCOw67GP97MWzceyFw+n9Vg== +-----END CERTIFICATE----- diff --git a/development/sp3-local/cert/ssp-hub-sp3.pem b/development/sp3-local/cert/ssp-hub-sp3.pem new file mode 100644 index 00000000..14cce0d7 --- /dev/null +++ b/development/sp3-local/cert/ssp-hub-sp3.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDS76ZdZLy9SQqO +0lwrWF3SIZba9tO9iiyDIN2oKVxLPCltYmDUDUKG6JAh/tL7ltYvYA3jMyh6b8lO +ftwT2hy2pTfhkkDI/9x6ymkD0bncX2Afhd2g+Cdaxvy91aigMNST5qxztqH9fAqL +FA1wQs8NmQ5trn6mqgoaMzF8vslDr4/osH7Ei3O8sFdTj6idsY8gXqmZnxLvia3Q +nrZjqd7jQEfHwV2u0GB2TlMVwN51bT2tEmtMWmLeEZJSRyOqPzgH6gsBvTxnRIeg +QsynliHtODlBnZz5Lwjju1CNXvas2FRiEjIW4ibNKDbtHtpEnomPqwrjMSDQCLhW +E7hGaDLPAgMBAAECggEBALkdC5kmkORkt1lDjxOTBzMjuyoKNyQ9oHarXxr2wUJd +V9Xg4iz2Pg37BpJu+WVFqE4HM+jRupJIjBfRCP57CXvYXsQc/7HlqO4xuBtb8IpP +QSIo7qkXXiIyQxet68A5WjU52NnryxmTxATt4iVE3ESIr7rdydQloZwAlUtue15j +mWOyznPK+l4lOeRyibYtFGoHzp4cdhUU2oWxFOEht7F4aGz2SMVGkFfrnRAShmyO +DYliGhEAVFWfnQwXsggXp7c0uBuvrb/kYZc1z6THQ4COBbzbp6NPOJu4yRez+IIs +lNbSYaV9N5EduNt5yayOI1s1Fi8wRm30f+Mgcb/zT2ECgYEA9/P0kl/Tho6ixTnx +furvdvzEW218UderluBooAzEmPKyy1rQQ/rPOcnGYg6SccYSFnJpp5LC128BpQWe +3v4j5c2XsK99PtP5aem5s2NsiZH/ehuTAJnXz1korG5hVGv+bpqK5KhaB7/a3Tm0 +JxVo/fx6iHsHXDGcxAiCzgy36+kCgYEA2cgmrXzqyK9SyPTuFgDjm750h9B7poyJ +DICmoo6FzXPHfjxJyP0xOXtq+nT0ujEDoq/kxs8iLCCW+FSfFuzITylg//0zESvE +9mcfMc6UFO8rS088CtyrsqtIOWG5xYOjrcUA92tiqkWMnklssFxjeNIrojsf/sIJ +AdzYXmx6zfcCgYEA6YP2jLf0xV+lyesFFgt6VOw+nQBiuc1My34y6rC7onPHkR7I +z4zxBrKRxB2HK+FnfX5pJKliGHRx7xF5Cvf7pNxYBM1xPe9ykJ3PBzQWrwUxvrUj +X8iDZ8LHPIWD4ncGmvGu5yPqDixQmlJS6RAP3kuettRvHROYWULOtfFicakCgYAJ +hhk66QWTdSdXpm5rA+rwOqn57oIZzHeJ1m5zGWx8iZ2lxZkscvYeH2mUPl0db1tL +WAnXL+O8rkgr3/d9FynDXHnjd/0tuQ5KAER69x++sp7gEjz79J6Fl7v21nE7VABq +bv0V1Nphu9zkZy2boM6wz/Acjh1eFLo0HKZRqsjMDQKBgQDDiB5QL94aVx/NTJ+y +FWTUmzwJ4HvnOxh2bmFomtU76FhevbK/R6aq85gHPQuD541KpIobTGBJlk9BPelX +V6BicmA/DGxd8aCg99Bn8haTUBTUNAWQJ5FdFmICHY5xul3oHHAs33FCNnx1ETOY +uA/4kr7SqT4LqoMXbsth3UHQhA== +-----END PRIVATE KEY----- diff --git a/development/sp3-local/config/authsources.php b/development/sp3-local/config/authsources.php new file mode 100644 index 00000000..4559d445 --- /dev/null +++ b/development/sp3-local/config/authsources.php @@ -0,0 +1,53 @@ + [ + // The default is to use core:AdminPassword, but it can be replaced with + // any authentication source. + + 'core:AdminPassword', + ], + + + // An authentication source which can authenticate against both SAML 2.0 + // and Shibboleth 1.3 IdPs. + 'ssp-hub' => [ + 'saml:SP', + + // The entity ID of this SP. + // Can be NULL/unset, in which case an entity ID is generated based on the metadata URL. + 'entityID' => 'http://ssp-sp3.local', + + // The entity ID of the IdP this should SP should contact. + // Can be NULL/unset, in which case the user will be shown a list of available IdPs. + 'idp' => 'ssp-hub.local', + + // The URL to the discovery service. + // Can be NULL/unset, in which case a builtin discovery service will be used. + 'discoURL' => null, + + // Specify what private key to use (such as for decrypting assertions). + 'privatekey' => 'ssp-hub-sp3.pem', + ], + + 'ssp-hub-custom-port' => [ + 'saml:SP', + + // The entity ID of this SP. + // Can be NULL/unset, in which case an entity ID is generated based on the metadata URL. + 'entityID' => 'http://ssp-sp3.local:8083', + + // The entity ID of the IdP this should SP should contact. + // Can be NULL/unset, in which case the user will be shown a list of available IdPs. + 'idp' => 'ssp-hub.local', + + // The URL to the discovery service. + // Can be NULL/unset, in which case a builtin discovery service will be used. + 'discoURL' => null, + + // Specify what private key to use (such as for decrypting assertions). + 'privatekey' => 'ssp-hub-sp3.pem', + ], +]; diff --git a/development/sp3-local/config/config.php b/development/sp3-local/config/config.php new file mode 100644 index 00000000..1686e5a2 --- /dev/null +++ b/development/sp3-local/config/config.php @@ -0,0 +1,844 @@ + SimpleSAML\Logger::ERR, // No statistics, only errors + 'WARNING' => SimpleSAML\Logger::WARNING, // No statistics, only warnings/errors + 'NOTICE' => SimpleSAML\Logger::NOTICE, // Statistics and errors + 'INFO' => SimpleSAML\Logger::INFO, // Verbose logs + 'DEBUG' => SimpleSAML\Logger::DEBUG, // Full debug logs - not recommended for production +]; + +/* + * Get config settings from ENV vars or set defaults + */ + +// Required to be defined in environment +$ADMIN_EMAIL = Env::get('ADMIN_EMAIL'); +$ADMIN_PASS = Env::get('ADMIN_PASS'); +$SECRET_SALT = Env::get('SECRET_SALT'); + +// Defaults provided if not defined in environment +$BASE_URL_PATH = Env::get('BASE_URL_PATH', '/'); +$ADMIN_NAME = Env::get('ADMIN_NAME', 'SAML Admin'); +$ADMIN_PROTECT_INDEX_PAGE = Env::get('ADMIN_PROTECT_INDEX_PAGE', true); +$SHOW_SAML_ERRORS = Env::get('SHOW_SAML_ERRORS', false); +$TIMEZONE = Env::get('TIMEZONE', 'GMT'); +$ENABLE_DEBUG = Env::get('ENABLE_DEBUG', false); +$LOGGING_LEVEL = Env::get('LOGGING_LEVEL', 'NOTICE'); +$LOGGING_HANDLER = Env::get('LOGGING_HANDLER', 'stderr'); +$SESSION_DURATION = (int)(Env::get('SESSION_DURATION', 540)); +$SESSION_DATASTORE_TIMEOUT = (int)(Env::get('SESSION_DATASTORE_TIMEOUT', (4 * 60 * 60))); // 4 hours +$SESSION_STATE_TIMEOUT = (int)(Env::get('SESSION_STATE_TIMEOUT', (60 * 60))); // 1 hour +$SESSION_COOKIE_LIFETIME = (int)(Env::get('SESSION_COOKIE_LIFETIME', 0)); +$SESSION_REMEMBERME_LIFETIME = (int)(Env::get('SESSION_REMEMBERME_LIFETIME', (14 * 86400))); // 14 days +$SECURE_COOKIE = Env::get('SECURE_COOKIE', true); +$THEME_USE = Env::get('THEME_USE', 'default'); +$SAML20_IDP_ENABLE = Env::get('SAML20_IDP_ENABLE', true); +$GOOGLE_ENABLE = Env::get('GOOGLE_ENABLE', false); + +$config = [ + + /* + * Setup the following parameters to match the directory of your installation. + * See the user manual for more details. + * + * Valid format for baseurlpath is: + * [(http|https)://(hostname|fqdn)[:port]]/[path/to/simplesaml/] + * (note that it must end with a '/') + * + * The full url format is useful if your simpleSAMLphp setup is hosted behind + * a reverse proxy. In that case you can specify the external url here. + * + * Please note that simpleSAMLphp will then redirect all queries to the + * external url, no matter where you come from (direct access or via the + * reverse proxy). + */ + 'baseurlpath' => $BASE_URL_PATH, + 'certdir' => 'cert/', + 'loggingdir' => 'log/', + 'datadir' => 'data/', + + /* + * A directory where simpleSAMLphp can save temporary files. + * + * SimpleSAMLphp will attempt to create this directory if it doesn't exist. + */ + 'tempdir' => '/tmp/simplesaml', + + + /* + * The 'debug' option allows you to control how SimpleSAMLphp behaves in certain + * situations where further action may be taken + * + * It can be left unset, in which case, debugging is switched off for all actions. + * If set, it MUST be an array containing the actions that you want to enable, or + * alternatively a hashed array where the keys are the actions and their + * corresponding values are booleans enabling or disabling each particular action. + * + * SimpleSAMLphp provides some pre-defined actions, though modules could add new + * actions here. Refer to the documentation of every module to learn if they + * allow you to set any more debugging actions. + * + * The pre-defined actions are: + * + * - 'saml': this action controls the logging of SAML messages exchanged with other + * entities. When enabled ('saml' is present in this option, or set to true), all + * SAML messages will be logged, including plaintext versions of encrypted + * messages. + * + * - 'backtraces': this action controls the logging of error backtraces. If you + * want to log backtraces so that you can debug any possible errors happening in + * SimpleSAMLphp, enable this action (add it to the array or set it to true). + * + * - 'validatexml': this action allows you to validate SAML documents against all + * the relevant XML schemas. SAML 1.1 messages or SAML metadata parsed with + * the XML to SimpleSAMLphp metadata converter or the metaedit module will + * validate the SAML documents if this option is enabled. + * + * If you want to disable debugging completely, unset this option or set it to an + * empty array. + */ + 'debug' => [ + 'saml' => $ENABLE_DEBUG, + 'backtraces' => true, + 'validatexml' => $ENABLE_DEBUG, + ], + + /* + * When showerrors is enabled, all error messages and stack traces will be output + * to the browser. + * + * When errorreporting is enabled, a form will be presented for the user to report + * the error to technicalcontact_email. + */ + 'showerrors' => $SHOW_SAML_ERRORS, + 'errorreporting' => false, + + /* + * Custom error show function called from SimpleSAML_Error_Error::show. + * See docs/simplesamlphp-errorhandling.txt for function code example. + * + * Example: + * 'errors.show_function' => array('sspmod_example_Error_Show', 'show'), + */ + + /* + * This option allows you to enable validation of XML data against its + * schemas. A warning will be written to the log if validation fails. + */ + 'debug.validatexml' => false, + + /* + * This password must be kept secret, and modified from the default value 123. + * This password will give access to the installation page of simpleSAMLphp with + * metadata listing and diagnostics pages. + * You can also put a hash here; run "bin/pwgen.php" to generate one. + */ + 'auth.adminpassword' => $ADMIN_PASS, + 'admin.protectindexpage' => $ADMIN_PROTECT_INDEX_PAGE, + 'admin.protectmetadata' => true, + + /* + * This is a secret salt used by simpleSAMLphp when it needs to generate a secure hash + * of a value. It must be changed from its default value to a secret value. The value of + * 'secretsalt' can be any valid string of any length. + * + * A possible way to generate a random salt is by running the following command from a unix shell: + * tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo + */ + 'secretsalt' => $SECRET_SALT, + + /* + * Some information about the technical persons running this installation. + * The email address will be used as the recipient address for error reports, and + * also as the technical contact in generated metadata. + */ + 'technicalcontact_name' => $ADMIN_NAME, + 'technicalcontact_email' => $ADMIN_EMAIL, + + /* + * The timezone of the server. This option should be set to the timezone you want + * simpleSAMLphp to report the time in. The default is to guess the timezone based + * on your system timezone. + * + * See this page for a list of valid timezones: http://php.net/manual/en/timezones.php + */ + 'timezone' => $TIMEZONE, + + /* + * Logging. + * + * define the minimum log level to log + * SimpleSAML\Logger::ERR No statistics, only errors + * SimpleSAML\Logger::WARNING No statistics, only warnings/errors + * SimpleSAML\Logger::NOTICE Statistics and errors + * SimpleSAML\Logger::INFO Verbose logs + * SimpleSAML\Logger::DEBUG Full debug logs - not reccomended for production + * + * Choose logging handler. + * + * Options: [syslog,file,errorlog,stderr] + * + */ + 'logging.level' => $logLevels[$LOGGING_LEVEL], + 'logging.handler' => $LOGGING_HANDLER, + + /* + * Specify the format of the logs. Its use varies depending on the log handler used (for instance, you cannot + * control here how dates are displayed when using the syslog or errorlog handlers), but in general the options + * are: + * + * - %date{}: the date and time, with its format specified inside the brackets. See the PHP documentation + * of the strftime() function for more information on the format. If the brackets are omitted, the standard + * format is applied. This can be useful if you just want to control the placement of the date, but don't care + * about the format. + * + * - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname' + * option below. + * + * - %level: the log level (name or number depending on the handler used). + * + * - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind + * the trailing space). + * + * - %trackid: the track ID, an identifier that allows you to track a single session. + * + * - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the + * $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header. + * + * - %msg: the message to be logged. + * + */ + //'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg', + + /* + * Choose which facility should be used when logging with syslog. + * + * These can be used for filtering the syslog output from simpleSAMLphp into its + * own file by configuring the syslog daemon. + * + * See the documentation for openlog (http://php.net/manual/en/function.openlog.php) for available + * facilities. Note that only LOG_USER is valid on windows. + * + * The default is to use LOG_LOCAL5 if available, and fall back to LOG_USER if not. + */ + 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, + + /* + * The process name that should be used when logging to syslog. + * The value is also written out by the other logging handlers. + */ + 'logging.processname' => 'simplesamlphp', + + /* Logging: file - Logfilename in the loggingdir from above. + */ + 'logging.logfile' => 'simplesamlphp.log', + + /* (New) statistics output configuration. + * + * This is an array of outputs. Each output has at least a 'class' option, which + * selects the output. + */ + 'statistics.out' => [// Log statistics to the normal log. + /* + [ + 'class' => 'core:Log', + 'level' => 'notice', + ], + */ + // Log statistics to files in a directory. One file per day. + /* + [ + 'class' => 'core:File', + 'directory' => '/var/log/stats', + ], + */ + ], + + + /* + * Enable + * + * Which functionality in simpleSAMLphp do you want to enable. Normally you would enable only + * one of the functionalities below, but in some cases you could run multiple functionalities. + * In example when you are setting up a federation bridge. + */ + 'enable.saml20-idp' => $SAML20_IDP_ENABLE, + 'enable.shib13-idp' => false, + 'enable.adfs-idp' => false, + 'enable.wsfed-sp' => false, + 'enable.authmemcookie' => false, + + + /* + * Module enable configuration + * + * Configuration to override module enabling/disabling. + * + * Example: + * + * 'module.enable' => array( + * // Setting to TRUE enables. + * 'exampleauth' => TRUE, + * // Setting to FALSE disables. + * 'saml' => FALSE, + * // Unset or NULL uses default. + * 'core' => NULL, + * ), + * + */ + + 'module.enable' => [ + // Setting to TRUE enables. + 'authgoogle' => $GOOGLE_ENABLE, + 'expirychecker' => true, + 'material' => true, + 'mfa' => true, + 'profilereview' => true, + 'silauth' => true, + 'sildisco' => true, + ], + + /* + * This value is the duration of the session in seconds. Make sure that the time duration of + * cookies both at the SP and the IdP exceeds this duration. + */ + 'session.duration' => $SESSION_DURATION, + + /* + * Sets the duration, in seconds, data should be stored in the datastore. As the datastore is used for + * login and logout requests, thid option will control the maximum time these operations can take. + * The default is 4 hours (4*60*60) seconds, which should be more than enough for these operations. + */ + 'session.datastore.timeout' => $SESSION_DATASTORE_TIMEOUT, + + /* + * Sets the duration, in seconds, auth state should be stored. + */ + 'session.state.timeout' => $SESSION_STATE_TIMEOUT, + + /* + * Option to override the default settings for the session cookie name + */ + 'session.cookie.name' => 'SSPSESSID', + + /* + * Expiration time for the session cookie, in seconds. + * + * Defaults to 0, which means that the cookie expires when the browser is closed. + * + * Example: + * 'session.cookie.lifetime' => 30*60, + */ + 'session.cookie.lifetime' => $SESSION_COOKIE_LIFETIME, + + /* + * Limit the path of the cookies. + * + * Can be used to limit the path of the cookies to a specific subdirectory. + * + * Example: + * 'session.cookie.path' => '/simplesaml/', + */ + 'session.cookie.path' => '/', + + /* + * Cookie domain. + * + * Can be used to make the session cookie available to several domains. + * + * Example: + * 'session.cookie.domain' => '.example.org', + */ + 'session.cookie.domain' => null, + + /* + * Set the secure flag in the cookie. + * + * Set this to TRUE if the user only accesses your service + * through https. If the user can access the service through + * both http and https, this must be set to FALSE. + */ + 'session.cookie.secure' => $SECURE_COOKIE, + + /* + * When set to FALSE fallback to transient session on session initialization + * failure, throw exception otherwise. + */ + 'session.disable_fallback' => false, + + /* + * Enable secure POST from HTTPS to HTTP. + * + * If you have some SP's on HTTP and IdP is normally on HTTPS, this option + * enables secure POSTing to HTTP endpoint without warning from browser. + * + * For this to work, module.php/core/postredirect.php must be accessible + * also via HTTP on IdP, e.g. if your IdP is on + * https://idp.example.org/ssp/, then + * http://idp.example.org/ssp/module.php/core/postredirect.php must be accessible. + */ + 'enable.http_post' => false, + + /* + * Options to override the default settings for php sessions. + */ + 'session.phpsession.cookiename' => null, + 'session.phpsession.savepath' => null, + 'session.phpsession.httponly' => true, + + /* + * Option to override the default settings for the auth token cookie + */ + 'session.authtoken.cookiename' => 'SSPAUTHTOKEN', + + /* + * Options for remember me feature for IdP sessions. Remember me feature + * has to be also implemented in authentication source used. + * + * Option 'session.cookie.lifetime' should be set to zero (0), i.e. cookie + * expires on browser session if remember me is not checked. + * + * Session duration ('session.duration' option) should be set according to + * 'session.rememberme.lifetime' option. + * + * It's advised to use remember me feature with session checking function + * defined with 'session.check_function' option. + */ + 'session.rememberme.enable' => false, + 'session.rememberme.checked' => false, + 'session.rememberme.lifetime' => $SESSION_REMEMBERME_LIFETIME, + + /** + * Custom function for session checking called on session init and loading. + * See docs/simplesamlphp-advancedfeatures.txt for function code example. + * + * Example: + * 'session.check_function' => array('sspmod_example_Util', 'checkSession'), + */ + + /* + * Languages available, RTL languages, and what language is default + */ + 'language.available' => array( + 'en', 'es', 'fr', 'pt', + ), + 'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'), + 'language.default' => 'en', + + /* + * Options to override the default settings for the language parameter + */ + 'language.parameter.name' => 'language', + 'language.parameter.setcookie' => true, + + /* + * Options to override the default settings for the language cookie + */ + 'language.cookie.name' => 'language', + 'language.cookie.domain' => null, + 'language.cookie.path' => '/', + 'language.cookie.lifetime' => (60 * 60 * 24 * 900), + + /** + * Custom getLanguage function called from SimpleSAML_XHTML_Template::getLanguage(). + * Function should return language code of one of the available languages or NULL. + * See SimpleSAML_XHTML_Template::getLanguage() source code for more info. + * + * This option can be used to implement a custom function for determining + * the default language for the user. + * + * Example: + * 'language.get_language_function' => array('sspmod_example_Template', 'getLanguage'), + */ + + /* + * Extra dictionary for attribute names. + * This can be used to define local attributes. + * + * The format of the parameter is a string with :. + * + * Specifying this option will cause us to look for modules//dictionaries/.definition.json + * The dictionary should look something like: + * + * { + * "firstattribute": { + * "en": "English name", + * "no": "Norwegian name" + * }, + * "secondattribute": { + * "en": "English name", + * "no": "Norwegian name" + * } + * } + * + * Note that all attribute names in the dictionary must in lowercase. + * + * Example: 'attributes.extradictionary' => 'ourmodule:ourattributes', + */ + 'attributes.extradictionary' => null, + + /* + * Which theme directory should be used? + */ + 'theme.use' => $THEME_USE, + + + /* + * Default IdP for WS-Fed. + */ + // 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost', + + /* + * Whether the discovery service should allow the user to save his choice of IdP. + */ + 'idpdisco.enableremember' => true, + 'idpdisco.rememberchecked' => true, + + // Disco service only accepts entities it knows. + 'idpdisco.validate' => true, + + 'idpdisco.extDiscoveryStorage' => null, + + /* + * IdP Discovery service look configuration. + * Wether to display a list of idp or to display a dropdown box. For many IdP' a dropdown box + * gives the best use experience. + * + * When using dropdown box a cookie is used to highlight the previously chosen IdP in the dropdown. + * This makes it easier for the user to choose the IdP + * + * Options: [links,dropdown] + * + */ + 'idpdisco.layout' => 'links', + + /* + * Whether simpleSAMLphp should sign the response or the assertion in SAML 1.1 authentication + * responses. + * + * The default is to sign the assertion element, but that can be overridden by setting this + * option to TRUE. It can also be overridden on a pr. SP basis by adding an option with the + * same name to the metadata of the SP. + */ + 'shib13.signresponse' => true, + + + /* + * Authentication processing filters that will be executed for all IdPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.idp' => [ + /* Enable the authproc filter below to add URN Prefixces to all attributes + 10 => array( + 'class' => 'core:AttributeMap', 'addurnprefix' + ), */ + /* Enable the authproc filter below to automatically generated eduPersonTargetedID. + 20 => 'core:TargetedID', + */ + + // Adopts language from attribute to use in UI + 30 => 'core:LanguageAdaptor', + + /* Add a realm attribute from edupersonprincipalname + 40 => 'core:AttributeRealm', + */ + 45 => [ + 'class' => 'core:StatisticsWithAttribute', + 'attributename' => 'realm', + 'type' => 'saml20-idp-SSO', + ], + + // If no attributes are requested in the SP metadata, then these will be sent through + 50 => [ + 'class' => 'core:AttributeLimit', + 'default' => true, + 'eduPersonPrincipalName', 'sn', 'givenName', 'mail', + ], + + // Use the uid value to populate the nameid entry + 60 => [ + 'class' => 'saml:AttributeNameID', + 'attribute' => 'uid', + 'Format' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + ], + + /* + * Search attribute "distinguishedName" for pattern and replaces if found + + 70 => array( + 'class' => 'core:AttributeAlter', + 'pattern' => '/OU=studerende/', + 'replacement' => 'Student', + 'subject' => 'distinguishedName', + '%replace', + ), + */ + + /* + * Consent module is enabled (with no permanent storage, using cookies). + + 90 => array( + 'class' => 'consent:Consent', + 'store' => 'consent:Cookie', + 'focus' => 'yes', + 'checked' => TRUE + ), + */ + + + // If language is set in Consent module it will be added as an attribute. + 99 => 'core:LanguageAdaptor', + ], + /* + * Authentication processing filters that will be executed for all SPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.sp' => [ + /* + 10 => array( + 'class' => 'core:AttributeMap', 'removeurnprefix' + ), + */ + + /* + * Generate the 'group' attribute populated from other variables, including eduPersonAffiliation. + 60 => array( + 'class' => 'core:GenerateGroups', 'eduPersonAffiliation' + ), + */ + /* + * All users will be members of 'users' and 'members' + 61 => array( + 'class' => 'core:AttributeAdd', 'groups' => array('users', 'members') + ), + */ + + // Adopts language from attribute to use in UI + 90 => 'core:LanguageAdaptor', + + ], + + + /* + * This option configures the metadata sources. The metadata sources is given as an array with + * different metadata sources. When searching for metadata, simpleSAMPphp will search through + * the array from start to end. + * + * Each element in the array is an associative array which configures the metadata source. + * The type of the metadata source is given by the 'type' element. For each type we have + * different configuration options. + * + * Flat file metadata handler: + * - 'type': This is always 'flatfile'. + * - 'directory': The directory we will load the metadata files from. The default value for + * this option is the value of the 'metadatadir' configuration option, or + * 'metadata/' if that option is unset. + * + * XML metadata handler: + * This metadata handler parses an XML file with either an EntityDescriptor element or an + * EntitiesDescriptor element. The XML file may be stored locally, or (for debugging) on a remote + * web server. + * The XML hetadata handler defines the following options: + * - 'type': This is always 'xml'. + * - 'file': Path to the XML file with the metadata. + * - 'url': The URL to fetch metadata from. THIS IS ONLY FOR DEBUGGING - THERE IS NO CACHING OF THE RESPONSE. + * + * + * Examples: + * + * This example defines two flatfile sources. One is the default metadata directory, the other + * is a metadata directory with autogenerated metadata files. + * + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'flatfile', 'directory' => 'metadata-generated'), + * ), + * + * This example defines a flatfile source and an XML source. + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'xml', 'file' => 'idp.example.org-idpMeta.xml'), + * ), + * + * + * Default: + * 'metadata.sources' => array( + * array('type' => 'flatfile') + * ), + */ + 'metadata.sources' => [ + ['type' => 'flatfile'], + ], + + + /* + * Configure the datastore for simpleSAMLphp. + * + * - 'phpsession': Limited datastore, which uses the PHP session. + * - 'memcache': Key-value datastore, based on memcache. + * - 'sql': SQL datastore, using PDO. + * + * The default datastore is 'phpsession'. + * + * (This option replaces the old 'session.handler'-option.) + */ + 'store.type' => 'phpsession', + + + /* + * The DSN the sql datastore should connect to. + * + * See http://www.php.net/manual/en/pdo.drivers.php for the various + * syntaxes. + */ + 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3', + + /* + * The username and password to use when connecting to the database. + */ + 'store.sql.username' => null, + 'store.sql.password' => null, + + /* + * The prefix we should use on our tables. + */ + 'store.sql.prefix' => 'simpleSAMLphp', + + + /* + * Configuration for the MemcacheStore class. This allows you to store + * multiple redudant copies of sessions on different memcache servers. + * + * 'memcache_store.servers' is an array of server groups. Every data + * item will be mirrored in every server group. + * + * Each server group is an array of servers. The data items will be + * load-balanced between all servers in each server group. + * + * Each server is an array of parameters for the server. The following + * options are available: + * - 'hostname': This is the hostname or ip address where the + * memcache server runs. This is the only required option. + * - 'port': This is the port number of the memcache server. If this + * option isn't set, then we will use the 'memcache.default_port' + * ini setting. This is 11211 by default. + * - 'weight': This sets the weight of this server in this server + * group. http://php.net/manual/en/function.Memcache-addServer.php + * contains more information about the weight option. + * - 'timeout': The timeout for this server. By default, the timeout + * is 3 seconds. + * + * Example of redudant configuration with load balancing: + * This configuration makes it possible to lose both servers in the + * a-group or both servers in the b-group without losing any sessions. + * Note that sessions will be lost if one server is lost from both the + * a-group and the b-group. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'mc_a1'), + * array('hostname' => 'mc_a2'), + * ), + * array( + * array('hostname' => 'mc_b1'), + * array('hostname' => 'mc_b2'), + * ), + * ), + * + * Example of simple configuration with only one memcache server, + * running on the same computer as the web server: + * Note that all sessions will be lost if the memcache server crashes. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'localhost'), + * ), + * ), + * + */ + 'memcache_store.servers' => [ + [ + ['hostname' => 'localhost'], + ], + ], + + + /* + * This value is the duration data should be stored in memcache. Data + * will be dropped from the memcache servers when this time expires. + * The time will be reset every time the data is written to the + * memcache servers. + * + * This value should always be larger than the 'session.duration' + * option. Not doing this may result in the session being deleted from + * the memcache servers while it is still in use. + * + * Set this value to 0 if you don't want data to expire. + * + * Note: The oldest data will always be deleted if the memcache server + * runs out of storage space. + */ + 'memcache_store.expires' => 36 * (60 * 60), // 36 hours. + + + /* + * Should signing of generated metadata be enabled by default. + * + * Metadata signing can also be enabled for a individual SP or IdP by setting the + * same option in the metadata for the SP or IdP. + */ + 'metadata.sign.enable' => true, + + /* + * The default key & certificate which should be used to sign generated metadata. These + * are files stored in the cert dir. + * These values can be overridden by the options with the same names in the SP or + * IdP metadata. + * + * If these aren't specified here or in the metadata for the SP or IdP, then + * the 'certificate' and 'privatekey' option in the metadata will be used. + * if those aren't set, signing of metadata will fail. + */ + 'metadata.sign.privatekey' => 'ssp-hub-sp3.pem', + 'metadata.sign.privatekey_pass' => null, + 'metadata.sign.certificate' => 'ssp-hub-sp3.crt', + + + /* + * Proxy to use for retrieving URLs. + * + * Example: + * 'proxy' => 'tcp://proxy.example.com:5100' + */ + 'proxy' => null, + + /* + * Array of domains that are allowed when generating links or redirections + * to URLs. simpleSAMLphp will use this option to determine whether to + * to consider a given URL valid or not, but you should always validate + * URLs obtained from the input on your own (i.e. ReturnTo or RelayState + * parameters obtained from the $_REQUEST array). + * + * Set to NULL to disable checking of URLs. + * + * simpleSAMLphp will automatically add your own domain (either by checking + * it dinamically, or by using the domain defined in the 'baseurlpath' + * directive, the latter having precedence) to the list of trusted domains, + * in case this option is NOT set to NULL. In that case, you are explicitly + * telling simpleSAMLphp to verify URLs. + * + * Set to an empty array to disallow ALL redirections or links pointing to + * an external URL other than your own domain. + * + * Example: + * 'trusted.url.domains' => array('sp.example.com', 'app.example.com'), + */ + 'trusted.url.domains' => null, + +]; diff --git a/development/sp3-local/metadata/saml20-idp-remote.php b/development/sp3-local/metadata/saml20-idp-remote.php new file mode 100644 index 00000000..f518f827 --- /dev/null +++ b/development/sp3-local/metadata/saml20-idp-remote.php @@ -0,0 +1,19 @@ + 'http://ssp-hub.local/saml2/idp/SSOService.php', + 'SingleLogoutService' => 'http://ssp-hub.local/saml2/idp/SingleLogoutService.php', + 'certData' =>'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', + +]; + diff --git a/docker-compose.yml b/docker-compose.yml index 39e10f9c..6d5b23e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: ["/data/run-debug.sh"] ports: @@ -49,7 +50,10 @@ services: - ssp-hub.local - ssp-idp1.local - ssp-idp2.local + - ssp-idp3.local - ssp-sp1.local + - ssp-sp2.local + - ssp-sp3.local - pwmanager.local - test-browser environment: @@ -78,6 +82,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: ["/data/run-tests.sh"] test-browser: @@ -127,6 +132,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: /data/run-debug.sh ports: - "80:80" @@ -158,11 +164,8 @@ services: - ./development/idp-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php - # Misc. files needed - - ./development/enable-exampleauth-module.sh:/data/enable-exampleauth-module.sh - # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code - - ./development/idp-local/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -175,9 +178,9 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: > bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && - /data/enable-exampleauth-module.sh && /data/run.sh" ports: - "8085:80" @@ -191,9 +194,9 @@ services: ID_BROKER_ASSERT_VALID_IP: "false" ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" - MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL: "http://pwmanager.local:8084/module.php/core/authenticate.php?as=ssp-hub-custom-port" REMEMBER_ME_SECRET: "12345" - PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL: "http://pwmanager.local:8084/module.php/core/authenticate.php?as=ssp-hub-custom-port" PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" @@ -219,12 +222,15 @@ services: - ./development/idp2-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp2-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + # Local modules - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - command: /data/run.sh + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco ports: - "8086:80" environment: @@ -236,6 +242,38 @@ services: SHOW_SAML_ERRORS: "true" THEME_USE: "material:material" + ssp-idp3.local: + build: . + volumes: + # Utilize custom certs + - ./development/idp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/idp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + - ./development/idp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + + # Utilize custom metadata + - ./development/idp3-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php + - ./development/idp3-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + + # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + ports: + - "8087:80" + env_file: + - local.env + environment: + ADMIN_EMAIL: "john_doe@there.com" + ADMIN_PASS: "c" + SECRET_SALT: "h57fjem34fh*nsJFGNjweJ" + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + IDP_NAME: "IdP3" + ssp-sp1.local: build: . volumes: @@ -257,6 +295,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco ports: - "8081:80" environment: @@ -268,7 +307,7 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" - sp2: + ssp-sp2.local: build: . volumes: # Utilize custom certs @@ -286,6 +325,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco ports: - "8082:80" environment: @@ -297,6 +337,31 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" + ssp-sp3.local: + build: . + volumes: + # Utilize custom certs + - ./development/sp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + - ./development/sp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp3-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + ports: + - "8083:80" + env_file: + - local.env + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp3 + - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz3 + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + pwmanager.local: image: silintl/ssp-base:develop volumes: @@ -309,7 +374,7 @@ services: # Utilize custom metadata - ./development/sp-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php ports: - - "8083:80" + - "8084:80" environment: - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=sp1 @@ -359,6 +424,28 @@ services: MYSQL_USER: "user" MYSQL_PASSWORD: "pass" + dynamo: + image: cnadiminti/dynamodb-local + command: "-sharedDb -inMemory" + hostname: dynamo + ports: + - "8000:8000" + environment: + reschedule: on-node-failure + + init-dynamo: + image: garland/aws-cli-docker + command: "/init-dynamodb.sh" + volumes: + - ./development/init-dynamodb.sh:/init-dynamodb.sh + depends_on: + - dynamo + environment: + - AWS_ACCESS_KEY_ID=0 + - AWS_SECRET_ACCESS_KEY=0 + - AWS_DEFAULT_REGION=us-east-1 + - AWS_DYNAMODB_ENDPOINT=http://dynamo:8000 + networks: default: driver: bridge diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 00000000..e7f047c9 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,18 @@ +Four SPs, a hub (a combined IdP and SP) and three IdPs get spun up by docker-compose. In order for this to work, you will need to edit your hosts file to include entries for the following domains ... +* ssp-sp1.local # to be used with port 8081 +* ssp-sp2.local # to be used with port 8082 +* ssp-sp3.local # to be used with port 8083 +* pwmanager.local # to be used with port 8084 +* ssp-hub.local +* ssp-idp1.local # to be used with port 8085 +* ssp-idp2.local # to be used with port 8086 +* ssp-idp3.local # to be used with port 8087 + +The ./development folder holds various files needed by these containers. It's the ssp-hub.local container which is the focus and serves as the SimpleSAMLphp hub. + +### Who should see what? +* `ssp-sp1.local` should be able to see and authenticate through both `ssp-idp1.local` and `ssp-idp2.local` +* `ssp-sp2.local` should only be able to see and authenticate through `ssp-idp2.local` +* `ssp-sp3.local` should only be able to see and authenticate through `ssp-idp1.local` + +If a session authenticated through one of the IdP's that is not permitted for a certain SP, then the hub should force that SP to re-authenticate against the right IdP. diff --git a/docs/editing_authprocs.md b/docs/editing_authprocs.md new file mode 100644 index 00000000..42a62368 --- /dev/null +++ b/docs/editing_authprocs.md @@ -0,0 +1,40 @@ +The sildisco module includes a few Auth Procs that can be called from the `config.php` file or **SP or IdP metadata**. + +### AttributeMap.php + +Copies (rather than replaces) attributes according to an attribute map. + +### TagGroup.php + +Grabs the values of the `urn:oid:2.5.4.31` (member of) attribute and prepends them with `idp||`. +The idp's name value is taken from the saml20-idp-remote.php file. In particular, if the IdP's metadata entry includes a `'IDPNamespace'` value, that is used. Otherwise, if it includes a `'name'` value, that is used. Otherwise, it uses the entity id of the IdP. + +### AddIdp2NameId.php + +Grabs the value of the saml:sp:NameID and appends `@` to it. +The IdP's metadata needs to include an `'IDPNamespace'` entry with a string value that is alphanumeric with hyphens and underscores. + +In order for this to work, the SP needs to include a line in its authsources.php file in the Hub's entry ... + +` 'NameIDPolicy' => "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",` + +In addition, the IDP's sp-remote metadata stanza for the Hub needs to include ... + +` 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',` + +### TrackIdps.php + +Creates and/or appends to a session value ("sildisco:authentication", "authenticated_idps") the **entity id** of the latest **IdP** to be used for authentication. + +### LogUser.php + +Logs information (common name, eduPrincipalPersonalName, employee number, IdP, SP, time) about each successful login to an AWS Dynamodb table. +``` + 97 => [ + 'class' =>'sildisco:LogUser', + 'DynamoRegion' => 'us-east-1', + 'DynamoLogTable' => 'sildisco_prod_user-log', + ], +``` +The following config is not needed on AWS, but it is needed locally +'DynamoEndpoint' ex. http://dynamo:8000 diff --git a/docs/functional_testing.md b/docs/functional_testing.md new file mode 100644 index 00000000..651e216b --- /dev/null +++ b/docs/functional_testing.md @@ -0,0 +1,129 @@ +# Automated Testing + +This is done through behat acceptance tests + +Once your containers are up, in your VM run ... + +`> docker-compose run --rm test /data/run-integration-tests.sh` + +Or, if you need to run just one of the tests, run ... + +`> docker-compose run --rm test bash` + +then + +`$ vendor/bin/behat features/mfa.feature:7` + +The tests are found in `/features`. They are similar to the manual tests listed below. + +# Manual Testing +## Main SP authenticates through Main Idp. Third SP is also authenticated. Second SP must re-authenticate. +### Ensure main SP goes to discovery page and can login through the main IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 1 +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password (without the quotation marks). +* This should return you to the main SP at http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure third SP is also authenticated +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure second SP is forced to authenticate +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure third SP is still authenticated +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +## Second SP authenticates through Second Idp. Main SP is forced to discovery page but is also authenticated. Third SP must re-authenticate. +### Ensure second SP can login through the second IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure main SP goes to discovery page but is authenticated +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 2 +* This should return you to the main SP at http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure third SP is forced to authenticate +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +## Third SP authenticates through Main Idp. Main SP is forced to discovery page but is also authenticated. Second SP must re-authenticate. +### Ensure third SP can login through the main IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure main SP goes to discovery page but is authenticated +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 1 +* This should get you to http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure second SP is forced to authenticate +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +## Main SP authenticates through Second Idp. Second SP is also authenticated. Third SP must re-authenticate. +### Ensure main SP goes to discovery page and can login through the second IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 2 +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password +* This should return you to the main SP at http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure second SP is also authenticated +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure third SP is forced to authenticate +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure second SP is still authenticated +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +## Second SP authenticates through Second Idp. Main SP is forced to discovery page, chooses main IdP and must authenticate. +### Ensure second SP can login through the second IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure main SP goes to discovery page and must authenticate when choosing the main Idp +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 1 +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 00000000..8e36676b --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,4 @@ +The sildisco module is a module for simplesamlphp. It's main purpose is to allow a simplesamlphp hub to control which Service Providers (SP's) can see and authenticate through which Identity Providers (IdP's). + +It relies on some utilities found in [ssp-utilities](https://github.com/silinternational/ssp-utilities). + diff --git a/docs/the_hub.md b/docs/the_hub.md new file mode 100644 index 00000000..266826f7 --- /dev/null +++ b/docs/the_hub.md @@ -0,0 +1,40 @@ +The hub will need its certs, `config.php` and `authsources.php` files as a normal simplesamlphp installation. Examples of these can be found in the `./development/hub` folder. (Note the `discoURL` entry in the `authsources.php` file.) + +Other files it will need are as follows ... +* The files in the `./lib` folder will need to go into `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/lib` +* The files in the `./www` folder will need to go into `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/www` +* The `./sspoverrides/www_saml2_idp/SSOService.php` file will need overwrite the same out-of-the-box file in `/data/vendor/simplesamlphp/simplesamlphp/www/saml2/idp/` + +### Metadata files +The hub should use the `saml20-*-remote.php` files from [ssp-base](https://github.com/silinternational/ssp-base) in `/data/vendor/simplesamlphp/simplesamlphp/metadata/`. These pull in metadata from all the files named `idp-*.php` and `sp-*.php` respectively, including those in sub-folders. + +In order for forced re-authentication to be limited only to situations which warrant it, the `saml20-idp-hosted.php` file should include an authproc as such ... +> [ +> 'class' =>'sildisco:TrackIdps', +> ] + +#### IDP Remote metadata + +##### IDPNamespace +Each metadata stanza should include an `IDPNamespace` entry that includes no special characters. This is intended for namespacing the `NameId` value in the Auth Proc `AddIdp2NameId.php`. +It is also used by the `TagGroup.php` Auth Proc to convert group names into the form ... + +`idp||`. + +##### betaEnabled +An optional metadata entry is `betaEnabled`. +This will allow the IdP to be marked as `'enable' => true` when the user has a certain cookie ('beta_tester') that they would get from visiting `hub_domain/module.php/sildisco/betatest.php`. +The user would need to manually remove that cookie to be free of this effect. + +Sildisco does not otherwise deal with looking at the `'enable'` value. However, a theme for idp discovery may (e.g. simplesamlphp-module-material). + +##### SPList +In order to limit access to an IdP to only certain SP's, add an `'SPList'` array entry to the metadata for the IdP. The values of this array should match the `entity_id` values from the `sp-remote.php` metadata. + +##### excludeByDefault +If you want to require SP's to list a certain IdP in their IDPList entry in order to be able to access it, add `excludeByDefault => true` to that IdP's metadata. + +### Forced IdP discovery +The `.../lib/IdP/SAML2.php` file ensures that if an SP is allowed to access more than one IdP, then the user will be forced back to the IdP discovery page, even if they are already authenticated through one of those IdP's. + +The reason for this is to ensure that the user has a chance to decide which of their identities is used for that SP. diff --git a/features/Sp1Idp1Sp2Idp2Sp3.feature b/features/Sp1Idp1Sp2Idp2Sp3.feature new file mode 100644 index 00000000..be765055 --- /dev/null +++ b/features/Sp1Idp1Sp2Idp2Sp3.feature @@ -0,0 +1,33 @@ +Feature: Ensure I can login to Sp1 through Idp1, must login to Sp2 through Idp2 and am already logged in for Sp3. + + Scenario: Login to SP1 through IDP1 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP1" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP1 + + Scenario: After IDP1 login, go to SP2 through IDP2 + Given I have authenticated with IDP1 for SP1 + When I go to the SP2 login page + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 + + Scenario: After IDP1 login, go directly to SP3 without credentials + Given I have authenticated with IDP1 for SP1 + When I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP3" + And I click on the "IDP 1" tile + Then I should see my attributes on SP3 + + Scenario: Logout of IDP1 + Given I have authenticated with IDP1 for SP1 + When I log out of IDP1 + Then I should see "You have been logged out." + + Scenario: Logout of IDP2 + Given I have authenticated with IDP2 for SP2 + When I log out of IDP2 + Then I should see "You have been logged out." diff --git a/features/Sp1Idp2Sp2Sp3Idp1.feature b/features/Sp1Idp2Sp2Sp3Idp1.feature new file mode 100644 index 00000000..aba78e0d --- /dev/null +++ b/features/Sp1Idp2Sp2Sp3Idp1.feature @@ -0,0 +1,22 @@ +Feature: Ensure I can login to Sp1 through Idp2, am already logged in for Sp2, and must login to Sp3 through Idp1. + + Scenario: Login to SP1 through IDP2 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP1" + And I click on the "IDP 2" tile + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP1 + + Scenario: After IDP2 login, go directly to SP2 without credentials + Given I have authenticated with IDP2 for SP1 + When I go to the SP2 login page + Then I should see my attributes on SP2 + + Scenario: After IDP2 login, go to SP3 through IDP1 + Given I have authenticated with IDP2 for SP1 + When I go to the SP3 login page + And I should see "to continue to SP3" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP3 diff --git a/features/Sp2Idp2Sp1Idp1Sp3.feature b/features/Sp2Idp2Sp1Idp1Sp3.feature new file mode 100644 index 00000000..c1637991 --- /dev/null +++ b/features/Sp2Idp2Sp1Idp1Sp3.feature @@ -0,0 +1,24 @@ +Feature: Ensure I can login to Sp2 through Idp2, must login to Sp1 if I choose Idp1, and don't need to login for Sp3. + + Scenario: Login to SP2 through IDP2 + When I go to the SP2 login page + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 + + Scenario: Login to SP1 through IDP1 + Given I have authenticated with IDP2 for SP2 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP1 + + Scenario: After IDP2 login, go directly to SP3 without credentials + Given I have authenticated with IDP2 for SP2 + And I have authenticated with IDP1 for SP1 + And I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP3" + And I click on the "IDP 1" tile + Then I should see my attributes on SP3 + diff --git a/features/Sp2Idp2Sp1Idp2Sp3.feature b/features/Sp2Idp2Sp1Idp2Sp3.feature new file mode 100644 index 00000000..e54e3244 --- /dev/null +++ b/features/Sp2Idp2Sp1Idp2Sp3.feature @@ -0,0 +1,22 @@ +Feature: Ensure I can login to Sp2 through Idp2, get discovery page for Sp1, and must login to Sp3 through Idp1. + + Scenario: Login to SP2 through IDP2 + When I go to the SP2 login page + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 + + Scenario: Get discovery page for SP1 + Given I have authenticated with IDP2 for SP2 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I click on the "IDP 2" tile + Then I should see my attributes on SP1 + + Scenario: Must login to SP3 through IDP1 + Given I have authenticated with IDP2 for SP2 + When I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP3 + diff --git a/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature b/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature new file mode 100644 index 00000000..c038df65 --- /dev/null +++ b/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature @@ -0,0 +1,22 @@ +Feature: Ensure I can login to Sp3 through Idp1, get the discovery page for Sp1 and must login to Sp2 through Idp2. + + Scenario: login to SP3 using IDP1 + When I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP3" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP3 + + Scenario: having authenticated with IDP1 for SP3, go to SP1 via the discovery page + Given I have authenticated with IDP1 for SP3 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I click on the "IDP 1" tile + Then I should see my attributes on SP1 + + Scenario: having authenticated with IDP1 for SP3, login to SP2 using IDP2 + Given I have authenticated with IDP1 for SP3 + When I go to the SP2 login page + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 diff --git a/features/WwwMetadataCept.feature b/features/WwwMetadataCept.feature new file mode 100644 index 00000000..f8fb2eb4 --- /dev/null +++ b/features/WwwMetadataCept.feature @@ -0,0 +1,13 @@ +Feature: Ensure I see the hub's metadata page. + + Scenario: Show the hub's metadata page in default format + When I go to "http://ssp-hub.local/module.php/sildisco/metadata.php" + Then I should see "$metadata['ssp-hub.local']" + + Scenario: Show the hub's metadata page in XML format + When I go to "http://ssp-hub.local/module.php/sildisco/metadata.php?format=xml" + Then I should see the metadata in XML format + + Scenario: Show the hub's metadata page PHP format + When I go to "http://ssp-hub.local/module.php/sildisco/metadata.php?format=php" + Then I should see "$metadata['ssp-hub.local']" diff --git a/features/ZSp1Idp1BetaSp1Idp3.feature b/features/ZSp1Idp1BetaSp1Idp3.feature new file mode 100644 index 00000000..e82b9053 --- /dev/null +++ b/features/ZSp1Idp1BetaSp1Idp3.feature @@ -0,0 +1,13 @@ +Feature: Ensure I don't see IdP 3 at first, but after I go to the Beta Tester page I can see and login through IdP 3. + +Scenario: Normally the IdP3 is disabled + When I go to the "SP1" login page + And the url should match "sildisco/disco.php" + Then the "div" element should contain "IdP 3 coming soon" + +Scenario: After going to the "Beta Test" page, IdP3 is available for use + When I go to "http://ssp-hub.local/module.php/sildisco/betatest.php" + And I go to the "SP1" login page + And I click on the "IDP 3" tile + And I log in using my "IDP 3" credentials + Then I should see "test_admin@idp3.org" diff --git a/features/bootstrap/ExpiryContext.php b/features/bootstrap/ExpiryContext.php index 473ff18b..749c134b 100644 --- a/features/bootstrap/ExpiryContext.php +++ b/features/bootstrap/ExpiryContext.php @@ -9,9 +9,6 @@ */ class ExpiryContext extends FeatureContext { - protected $username = null; - protected $password = null; - /** * The browser session, used for interacting with the website. * @@ -64,27 +61,6 @@ protected function assertFormNotContains($text, $page) } } - /** - * Get the login button from the given page. - * - * @param DocumentElement $page The page. - * @return NodeElement - */ - protected function getLoginButton($page) - { - $buttons = $page->findAll('css', 'button'); - $loginButton = null; - foreach ($buttons as $button) { - $lcButtonText = strtolower($button->getText()); - if (strpos($lcButtonText, 'login') !== false) { - $loginButton = $button; - break; - } - } - Assert::assertNotNull($loginButton, 'Failed to find the login button'); - return $loginButton; - } - /** * @Given I provide credentials that will expire in the distant future */ @@ -95,24 +71,6 @@ public function iProvideCredentialsThatWillExpireInTheDistantFuture() $this->password = 'a'; } - /** - * @When I login - */ - public function iLogin() - { - $this->fillField('username', $this->username); - $this->fillField('password', $this->password); - $this->pressButton('Login'); - } - - /** - * @Then I should end up at my intended destination - */ - public function iShouldEndUpAtMyIntendedDestination() - { - $this->assertPageBodyContainsText('Your attributes'); - } - /** * @Given I provide credentials that will expire very soon */ @@ -132,34 +90,6 @@ public function iShouldSeeAWarningThatMyPasswordWillExpireSoon() Assert::assertContains('will expire', $page->getHtml()); } - /** - * Submit the login form, including the secondary page's form (if - * simpleSAMLphp shows another page because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitLoginForm($page) - { - $loginButton = $this->getLoginButton($page); - $loginButton->click(); - - // SimpleSAMLphp 1.15 markup for secondary page: - $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); - if ($postLoginSubmitButton instanceof NodeElement) { - $postLoginSubmitButton->click(); - } else { - - // SimpleSAMLphp 1.14 markup for secondary page: - $body = $page->find('css', 'body'); - if ($body instanceof NodeElement) { - $onload = $body->getAttribute('onload'); - if ($onload === "document.getElementsByTagName('input')[0].click();") { - $body->pressButton('Submit'); - } - } - } - } - /** * @Then there should be a way to go change my password now */ diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 25cda925..748733c7 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -1,11 +1,15 @@ session->getStatusCode() . '] '; $this->printLastResponse(); @@ -90,7 +98,7 @@ public function iLogInAsAHubAdministrator() $this->logInAs('admin', 'abc123'); } - private function logInAs(string $username, string $password) + protected function logInAs(string $username, string $password) { $this->fillField('username', $username); $this->fillField('password', $password); @@ -149,6 +157,9 @@ public function iGoToTheSpLoginPage($sp) case 'SP2': $this->visit(self::SP2_LOGIN_PAGE); break; + case 'SP3': + $this->visit(self::SP3_LOGIN_PAGE); + break; } } @@ -206,4 +217,118 @@ public function theFileShouldContain($filePath, PyStringNode $expectedJson) json_decode($expectedJson, true) ); } + + /** + * Get the login button from the given page. + * + * @param DocumentElement $page The page. + * @return NodeElement + */ + protected function getLoginButton($page) + { + $buttons = $page->findAll('css', 'button'); + $loginButton = null; + foreach ($buttons as $button) { + $lcButtonText = strtolower($button->getText()); + if (strpos($lcButtonText, 'login') !== false) { + $loginButton = $button; + break; + } + } + Assert::notNull($loginButton, 'Failed to find the login button'); + return $loginButton; + } + + /** + * @When I log in + */ + public function iLogIn() + { + $page = $this->session->getPage(); + try { + $page->fillField('username', $this->username); + $page->fillField('password', $this->password); + $this->submitLoginForm($page); + } catch (ElementNotFoundException $e) { + Assert::true(false, sprintf( + "Did not find that element in the page.\nError: %s\nPage content: %s", + $e->getMessage(), + $page->getContent() + )); + } + } + + /** + * @Given I have logged in (again) + */ + public function iHaveLoggedIn() + { + $this->iLogin(); + } + + /** + * Submit the current form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported) by + * clicking the specified button. + * + * @param string $buttonName The value of the desired button's `name` + * attribute. + */ + protected function submitFormByClickingButtonNamed($buttonName) + { + $page = $this->session->getPage(); + $button = $page->find('css', sprintf( + '[name=%s]', + $buttonName + )); + Assert::notNull($button, 'Failed to find button named ' . $buttonName); + $button->click(); + $this->submitSecondarySspFormIfPresent($page); + } + + /** + * Submit the login form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitLoginForm($page) + { + $loginButton = $this->getLoginButton($page); + $loginButton->click(); + $this->submitSecondarySspFormIfPresent($page); + } + + /** + * Submit the secondary page's form (if simpleSAMLphp shows another page + * because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitSecondarySspFormIfPresent($page) + { + // SimpleSAMLphp 1.15 markup for secondary page: + $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); + if ($postLoginSubmitButton instanceof NodeElement) { + $postLoginSubmitButton->click(); + } else { + + // SimpleSAMLphp 1.14 markup for secondary page: + $body = $page->find('css', 'body'); + if ($body instanceof NodeElement) { + $onload = $body->getAttribute('onload'); + if ($onload === "document.getElementsByTagName('input')[0].click();") { + $body->pressButton('Submit'); + } + } + } + } + + /** + * @Then I should end up at my intended destination + */ + public function iShouldEndUpAtMyIntendedDestination() + { + $this->assertPageBodyContainsText('Your attributes'); + } } diff --git a/features/bootstrap/LoginContext.php b/features/bootstrap/LoginContext.php index 61cc2e39..7425e2b7 100644 --- a/features/bootstrap/LoginContext.php +++ b/features/bootstrap/LoginContext.php @@ -37,15 +37,9 @@ class LoginContext extends FeatureContext /** @var IdBroker */ private $idBroker; - /** @var string|null */ - private $password = null; - /** @var Request */ private $request; - /** @var string|null */ - private $username = null; - /** * Initializes context. * diff --git a/features/bootstrap/MfaContext.php b/features/bootstrap/MfaContext.php index 78600561..16dbdaa0 100644 --- a/features/bootstrap/MfaContext.php +++ b/features/bootstrap/MfaContext.php @@ -12,9 +12,6 @@ */ class MfaContext extends FeatureContext { - protected $username = null; - protected $password = null; - const USER_AGENT_WITHOUT_WEBAUTHN_SUPPORT = 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko'; const USER_AGENT_WITH_WEBAUTHN_SUPPORT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36'; @@ -51,28 +48,7 @@ protected function getContinueButton($page) $continueButton = $page->find('css', '[name=continue]'); return $continueButton; } - - /** - * Get the login button from the given page. - * - * @param DocumentElement $page The page. - * @return NodeElement - */ - protected function getLoginButton($page) - { - $buttons = $page->findAll('css', 'button'); - $loginButton = null; - foreach ($buttons as $button) { - $lcButtonText = strtolower($button->getText()); - if (strpos($lcButtonText, 'login') !== false) { - $loginButton = $button; - break; - } - } - Assert::assertNotNull($loginButton, 'Failed to find the login button'); - return $loginButton; - } - + /** * Get the button for submitting the MFA form. * @@ -85,35 +61,7 @@ protected function getSubmitMfaButton($page) Assert::assertNotNull($submitMfaButton, 'Failed to find the submit-MFA button'); return $submitMfaButton; } - - /** - * @When I login - */ - public function iLogin() - { - $page = $this->session->getPage(); - try { - $page->fillField('username', $this->username); - $page->fillField('password', $this->password); - $this->submitLoginForm($page); - } catch (ElementNotFoundException $e) { - Assert::fail(sprintf( - "Did not find that element in the page.\nError: %s\nPage content: %s", - $e->getMessage(), - $page->getContent() - )); - } - } - - /** - * @Then I should end up at my intended destination - */ - public function iShouldEndUpAtMyIntendedDestination() - { - $page = $this->session->getPage(); - Assert::assertContains('Your attributes', $page->getHtml()); - } - + /** * Submit the current form, including the secondary page's form (if * simpleSAMLphp shows another page because JavaScript isn't supported) by @@ -133,21 +81,7 @@ protected function submitFormByClickingButtonNamed($buttonName) $button->click(); $this->submitSecondarySspFormIfPresent($page); } - - /** - * Submit the login form, including the secondary page's form (if - * simpleSAMLphp shows another page because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitLoginForm($page) - { - $loginButton = $this->getLoginButton($page); - $loginButton->click(); - $this->submitSecondarySspFormIfPresent($page); - } - - + /** * Submit the MFA form, including the secondary page's form (if * simpleSAMLphp shows another page because JavaScript isn't supported). @@ -161,33 +95,7 @@ protected function submitMfaForm($page) $this->submitSecondarySspFormIfPresent($page); } - - /** - * Submit the secondary page's form (if simpleSAMLphp shows another page - * because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitSecondarySspFormIfPresent($page) - { - // SimpleSAMLphp 1.15 markup for secondary page: - $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); - if ($postLoginSubmitButton instanceof NodeElement) { - $postLoginSubmitButton->click(); - } else { - - // SimpleSAMLphp 1.14 markup for secondary page: - $body = $page->find('css', 'body'); - if ($body instanceof NodeElement) { - $onload = $body->getAttribute('onload'); - if ($onload === "document.getElementsByTagName('input')[0].click();") { - $body->pressButton('Submit'); - } - } - } - } - - /** + /** * @Given I provide credentials that do not need MFA */ public function iProvideCredentialsThatDoNotNeedMfa() @@ -286,14 +194,6 @@ public function iShouldSeeAPromptForAWebAuthn() Assert::assertContains('

USB Security Key

', $page->getHtml()); } - /** - * @Given I have logged in (again) - */ - public function iHaveLoggedIn() - { - $this->iLogin(); - } - protected function submitMfaValue($mfaValue) { $page = $this->session->getPage(); diff --git a/features/bootstrap/ProfileReviewContext.php b/features/bootstrap/ProfileReviewContext.php index 8e60c0d3..9c88f526 100644 --- a/features/bootstrap/ProfileReviewContext.php +++ b/features/bootstrap/ProfileReviewContext.php @@ -10,11 +10,6 @@ */ class ProfileReviewContext extends FeatureContext { - protected $nonPwManagerUrl = 'http://sp/module.php/core/authenticate.php?as=profilereview-idp-no-port'; - - protected $username = null; - protected $password = null; - /** * Assert that the given page has a form that contains the given text. * @@ -37,55 +32,6 @@ protected function assertFormContains($text, $page) )); } - /** - * Get the login button from the given page. - * - * @param DocumentElement $page The page. - * @return NodeElement - */ - protected function getLoginButton($page) - { - $buttons = $page->findAll('css', 'button'); - $loginButton = null; - foreach ($buttons as $button) { - $lcButtonText = strtolower($button->getText()); - if (strpos($lcButtonText, 'login') !== false) { - $loginButton = $button; - break; - } - } - Assert::assertNotNull($loginButton, 'Failed to find the login button'); - return $loginButton; - } - - /** - * @When I login - */ - public function iLogin() - { - $page = $this->session->getPage(); - try { - $page->fillField('username', $this->username); - $page->fillField('password', $this->password); - $this->submitLoginForm($page); - } catch (ElementNotFoundException $e) { - Assert::fail(sprintf( - "Did not find that element in the page.\nError: %s\nPage content: %s", - $e->getMessage(), - $page->getContent() - )); - } - } - - /** - * @Then I should end up at my intended destination - */ - public function iShouldEndUpAtMyIntendedDestination() - { - $page = $this->session->getPage(); - Assert::assertContains('Your attributes', $page->getHtml()); - } - /** * Submit the current form, including the secondary page's form (if * simpleSAMLphp shows another page because JavaScript isn't supported) by @@ -105,45 +51,7 @@ protected function submitFormByClickingButtonNamed($buttonName) $button->click(); $this->submitSecondarySspFormIfPresent($page); } - - /** - * Submit the login form, including the secondary page's form (if - * simpleSAMLphp shows another page because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitLoginForm($page) - { - $loginButton = $this->getLoginButton($page); - $loginButton->click(); - $this->submitSecondarySspFormIfPresent($page); - } - - /** - * Submit the secondary page's form (if simpleSAMLphp shows another page - * because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitSecondarySspFormIfPresent($page) - { - // SimpleSAMLphp 1.15 markup for secondary page: - $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); - if ($postLoginSubmitButton instanceof NodeElement) { - $postLoginSubmitButton->click(); - } else { - - // SimpleSAMLphp 1.14 markup for secondary page: - $body = $page->find('css', 'body'); - if ($body instanceof NodeElement) { - $onload = $body->getAttribute('onload'); - if ($onload === "document.getElementsByTagName('input')[0].click();") { - $body->pressButton('Submit'); - } - } - } - } - + /** * @Given I provide credentials that do not need review */ @@ -176,13 +84,6 @@ public function iProvideCredentialsThatAreDueForAReminder($category, $nagType) } } - /** - * @Given I have logged in (again) - */ - public function iHaveLoggedIn() - { - $this->iLogin(); - } protected function pageContainsElementWithText($cssSelector, $text) { diff --git a/features/bootstrap/SilDiscoContext.php b/features/bootstrap/SilDiscoContext.php new file mode 100644 index 00000000..24781840 --- /dev/null +++ b/features/bootstrap/SilDiscoContext.php @@ -0,0 +1,120 @@ +username = 'sildisco_idp1'; + $this->password = 'sildisco_password'; + break; + + case 'IDP 2': + $this->username = 'sildisco_idp2'; + $this->password = 'sildisco_password'; + break; + + case 'IDP 3': + $this->username = 'admin'; + $this->password = 'c'; + break; + + default: + throw new \Exception('credential name not recognized'); + } + $this->iLogIn(); + } + + /** + * @Then I should see my attributes on :sp + */ + public function iShouldSeeMyAttributesOnSp($sp) + { + $currentUrl = $this->session->getCurrentUrl(); + Assert::assertStringStartsWith( + 'http://ssp-' . strtolower($sp), + $currentUrl, + 'Did NOT end up at ' . $sp + ); + $this->assertPageContainsText('Your attributes'); + } + + /** + * @When I login using password :password + */ + public function iLoginUsingPassword($password) + { + $this->logInAs('admin', $password); + } + + /** + * @Given I have authenticated with IDP1 for :sp + */ + public function iHaveAuthenticatedWithIdp1($sp) + { + $this->iGoToTheSpLoginPage($sp); + $this->iClickOnTheTile('IDP 1'); + $this->username = 'sildisco_idp1'; + $this->password = 'sildisco_password'; + $this->iLogIn(); + } + + /** + * @Given I have authenticated with IDP2 for :sp + */ + public function iHaveAuthenticatedWithIdp2($sp) + { + $this->iGoToTheSpLoginPage($sp); + if ($sp != "SP2") { // SP2 only has IDP2 in its IDPList + $this->iClickOnTheTile('IDP 2'); + } + $this->username = 'sildisco_idp2'; + $this->password = 'sildisco_password'; + $this->iLogIn(); + } + + /** + * @When I log out of IDP1 + */ + public function iLogOutOfIdp1() + { + $this->iGoToTheSpLoginPage('SP3'); + $this->iClickOnTheTile('IDP 1'); + $this->clickLink('Logout'); + $this->assertPageContainsText('You have been logged out.'); + } + + /** + * @When I log out of IDP2 + */ + public function iLogOutOfIdp2() + { + $this->iGoToTheSpLoginPage('SP2'); + $this->clickLink('Logout'); + $this->assertPageContainsText('You have been logged out.'); + } + + /** + * @Then I should see the metadata in XML format + */ + public function iShouldSeeTheMetadataInXmlFormat() + { + $contentType = $this->session->getResponseHeader('Content-Type'); + Assert::assertEquals('application/xml', $contentType); + + Assert::assertEquals(200, $this->session->getStatusCode()); + + $xml = file_get_contents($this->getSession()->getCurrentUrl()); + Assert::assertStringContainsString( + 'entityID="ssp-hub.local"', + $xml, + "page doesn't contain entityID" + ); + } + +} diff --git a/features/expirychecker.feature b/features/expirychecker.feature index c690015c..f93f7af4 100644 --- a/features/expirychecker.feature +++ b/features/expirychecker.feature @@ -5,29 +5,29 @@ Feature: Expiry Checker module Scenario: Password will expire in the distant future Given I provide credentials that will expire in the distant future - When I login + When I log in Then I should end up at my intended destination Scenario: Password will expire tomorrow Given I provide credentials that will expire very soon - When I login + When I log in Then I should see a warning that my password will expire soon And there should be a way to go change my password now And there should be a way to continue without changing my password Scenario: Password has expired Given I provide credentials that have expired - When I login + When I log in Then I should see a message that my password has expired And there should be a way to go change my password now But there should NOT be a way to continue without changing my password Scenario: Reject missing expiration date Given I provide credentials that have no password expiration date - When I login + When I log in Then I should see an error message Scenario: Reject invalid expiration date Given I provide credentials that have an invalid password expiration date - When I login + When I log in Then I should see an error message diff --git a/features/mfa.feature b/features/mfa.feature index 1ccdba3a..5a9c948f 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -6,37 +6,37 @@ Feature: Prompt for MFA credentials Scenario: Don't prompt for MFA Given I provide credentials that do not need MFA - When I login + When I log in Then I should end up at my intended destination Scenario: Needs MFA, but no MFA options are available Given I provide credentials that need MFA but have no MFA options available - When I login + When I log in Then I should see a message that I have to set up MFA And there should be a way to go set up MFA now And there should NOT be a way to continue to my intended destination Scenario: Following the requirement to go set up MFA Given I provide credentials that need MFA but have no MFA options available - And I login + And I log in When I click the set-up-MFA button Then I should end up at the mfa-setup URL And I should NOT be able to get to my intended destination Scenario: Needs MFA, has backup code option available Given I provide credentials that need MFA and have backup codes available - When I login + When I log in Then I should see a prompt for a backup code Scenario: Needs MFA, has TOTP option available Given I provide credentials that need MFA and have TOTP available - When I login + When I log in Then I should see a prompt for a TOTP code Scenario: Needs MFA, has WebAuthn option available Given I provide credentials that need MFA and have WebAuthn available And the user's browser supports WebAuthn - When I login + When I log in Then I should see a prompt for a WebAuthn security key Scenario: Accepting a (non-rate-limited) correct MFA value @@ -129,7 +129,7 @@ Feature: Prompt for MFA credentials Scenario Outline: Defaulting to another option when WebAuthn is not supported Given I provide credentials that have And the user's browser - When I login + When I log in Then I should see a prompt for a Examples: @@ -155,7 +155,7 @@ Feature: Prompt for MFA credentials Given I provide credentials that have a used And and I have a more recently used And the user's browser - When I login + When I log in Then I should see a prompt for a Examples: @@ -170,13 +170,13 @@ Feature: Prompt for MFA credentials Scenario: Defaulting to the manager code despite having a used mfa Given I provide credentials that have a manager code, a WebAuthn and a more recently used TOTP And the user's browser supports WebAuthn - When I login + When I log in Then I should see a prompt for a manager rescue code Scenario Outline: When to show the WebAuthn-not-supported error message Given I provide credentials that have WebAuthn And the user's browser - When I login + When I log in Then I see an error message about WebAuthn being unsupported Examples: @@ -188,7 +188,7 @@ Feature: Prompt for MFA credentials Scenario Outline: When to show the link to send a manager rescue code Given I provide credentials that have And the user a manager email - When I login + When I log in Then I see a link to send a code to the user's manager Examples: @@ -211,13 +211,13 @@ Feature: Prompt for MFA credentials Scenario: Ask for a code to be sent to my manager Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in When I click the Request Assistance link Then there should be a way to request a manager code Scenario: Submit a code sent to my manager at an earlier time Given I provide credentials that have a manager code - And I login + And I log in When I submit the correct manager code # because profile review is required after using a manager code: And I click the remind-me-later button @@ -226,7 +226,7 @@ Feature: Prompt for MFA credentials Scenario: Submit a correct manager code Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in And I click the Request Assistance link And I click the Send a code link When I submit the correct manager code @@ -237,7 +237,7 @@ Feature: Prompt for MFA credentials Scenario: Submit an incorrect manager code Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in And I click the Request Assistance link And I click the Send a code link When I submit an incorrect manager code @@ -246,7 +246,7 @@ Feature: Prompt for MFA credentials Scenario: Ask for assistance, but change my mind Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in And I click the Request Assistance link When I click the Cancel button Then I should see a prompt for a backup code diff --git a/features/profilereview.feature b/features/profilereview.feature index b378e712..9c3988d1 100644 --- a/features/profilereview.feature +++ b/features/profilereview.feature @@ -5,12 +5,12 @@ Feature: Prompt to review profile information Scenario: Don't ask for review Given I provide credentials that do not need review - When I login + When I log in Then I should end up at my intended destination Scenario Outline: Present reminder as required by the user profile Given I provide credentials that are due for a reminder - When I login + When I log in Then I should see the message: And there should be a way to go update my profile now And there should be a way to continue to my intended destination diff --git a/modules/sildisco/lib/Auth/Process/AddIdp2NameId.php b/modules/sildisco/lib/Auth/Process/AddIdp2NameId.php new file mode 100644 index 00000000..0f816211 --- /dev/null +++ b/modules/sildisco/lib/Auth/Process/AddIdp2NameId.php @@ -0,0 +1,175 @@ + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + * + */ +class AddIdp2NameId extends \SimpleSAML\Auth\ProcessingFilter { + + const IDP_KEY = "saml:sp:IdP"; // the key that points to the entity id in the state + + // the metadata key for the IDP's Namespace code (i.e. short name) + const IDP_CODE_KEY = 'IDPNamespace'; + + const DELIMITER = '@'; // The symbol between the NameID proper and the Idp code. + + const SP_NAMEID_ATTR = 'saml:sp:NameID'; // The key for the NameID + + const VALUE_KEY = 'Value'; // The value key for the NamedID entry + + const ERROR_PREFIX = "AddIdp2NameId: "; // Text to go at the beginning of error messages + + const FORMAT_KEY = 'Format'; + + /** + * What NameQualifier should be used. + * Can be one of: + * - a string: The qualifier to use. + * - FALSE: Do not include a NameQualifier. This is the default. + * - TRUE: Use the IdP entity ID. + * + * @var string|bool + */ + private $nameQualifier; + + + /** + * What SPNameQualifier should be used. + * Can be one of: + * - a string: The qualifier to use. + * - FALSE: Do not include a SPNameQualifier. + * - TRUE: Use the SP entity ID. This is the default. + * + * @var string|bool + */ + private $spNameQualifier; + + + /** + * The format of this NameID. + * + * This property must be initialized the subclass. + * + * @var string + */ + protected $format; + + + /** + * Initialize this filter, parse configuration. + * + * @param array $config Configuration information about this filter. + * @param mixed $reserved For future use. + */ + public function __construct($config, $reserved) { + parent::__construct($config, $reserved); + assert('is_array($config)'); + + if (isset($config['NameQualifier'])) { + $this->nameQualifier = $config['NameQualifier']; + } else { + $this->nameQualifier = false; + } + + if (isset($config['SPNameQualifier'])) { + $this->spNameQualifier = $config['SPNameQualifier']; + } else { + $this->spNameQualifier = true; + } + + $this->format = Null; + if ( ! empty($config[self::FORMAT_KEY])) { + $this->format = (string) $config[self::FORMAT_KEY]; + } + } + + /** + * @param $nameId \SAML2\XML\saml\NameID + * @param $IDPNamespace string + * + * Modifies the nameID object by adding text to the end of its value attribute + */ + public function appendIdp($nameId, $IDPNamespace) { + + $suffix = self::DELIMITER . $IDPNamespace; + $value = $nameId->getValue(); + $nameId->setValue($value . $suffix); + return; + } + + + /** + * Apply filter to copy attributes. + * + * @param array &$state The current state array + */ + public function process(&$state) { + assert('is_array($state)'); + + $samlIDP = $state[self::IDP_KEY]; + + if (empty($state[self::SP_NAMEID_ATTR])) { + \SimpleSAML\Logger::warning( + self::SP_NAMEID_ATTR . ' attribute not available from ' . + $samlIDP . '.' + ); + return; + } + + // Get the potential IDPs from idp remote metadata + $metadataPath = __DIR__ . '/../../../../../metadata'; + + // If a unit test sends a different metadataPath, use it + if (isset($state['metadataPath'])) { + $metadataPath = $state['metadataPath']; + } + $idpEntries = \Sil\SspUtils\Metadata::getIdpMetadataEntries($metadataPath); + + $idpEntry = $idpEntries[$samlIDP]; + + // The IDP metadata must have an IDPNamespace entry + if ( ! isset($idpEntry[self::IDP_CODE_KEY])) { + throw new \SimpleSAML\Error\Exception(self::ERROR_PREFIX . "Missing required metadata entry: " . + self::IDP_CODE_KEY . "."); + } + + // IDPNamespace must be a non-empty string + if ( ! is_string($idpEntry[self::IDP_CODE_KEY])) { + throw new \SimpleSAML\Error\Exception(self::ERROR_PREFIX . "Required metadata " . + "entry, " . self::IDP_CODE_KEY . ", must be a non-empty string."); + } + + // IDPNamespace must not have special characters in it + if ( ! preg_match("/^[A-Za-z0-9_-]+$/", $idpEntry[self::IDP_CODE_KEY])) { + throw new \SimpleSAML\Error\Exception(self::ERROR_PREFIX . "Required metadata " . + "entry, " . self::IDP_CODE_KEY . ", must not be empty or contain anything except " . + "letters, numbers, hyphens and underscores."); + } + + $IDPNamespace = $idpEntry[self::IDP_CODE_KEY]; + + $nameId = $state[self::SP_NAMEID_ATTR]; + self::appendIdp($nameId, $IDPNamespace); + + $format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'; + + if ( ! empty($this->format)) { + $format = $this->format; + } elseif ( ! empty($nameId->Format)) { + $format = $nameId->Format; + } + + $state['saml:NameID'][$format] = $nameId; + + } + +} diff --git a/modules/sildisco/lib/Auth/Process/LogUser.php b/modules/sildisco/lib/Auth/Process/LogUser.php new file mode 100644 index 00000000..c1cc2a55 --- /dev/null +++ b/modules/sildisco/lib/Auth/Process/LogUser.php @@ -0,0 +1,233 @@ +dynamoEndpoint = $config[self::DYNAMO_ENDPOINT_KEY] ?? null; + $this->dynamoRegion = $config[self::DYNAMO_REGION_KEY] ?? null; + $this->dynamoLogTable = $config[self::DYNAMO_LOG_TABLE_KEY] ?? null; + } + + /** + * Log info for a user's login to Dyanmodb + * + * @param array &$state The current state array + */ + public function process(&$state) { + if (! $this->configsAreValid()) { + return; + } + + $awsKey = getenv(self::AWS_ACCESS_KEY_ID_ENV); + if (! $awsKey ) { + \SimpleSAML\Logger::error(self::AWS_ACCESS_KEY_ID_ENV . " environment variable is required for LogUser."); + return; + } + $awsSecret = getenv(self::AWS_SECRET_ACCESS_KEY_ENV); + if (! $awsSecret ) { + \SimpleSAML\Logger::error(self::AWS_SECRET_ACCESS_KEY_ENV . " environment variable is required for LogUser."); + return; + } + + assert(is_array($state)); + + // Get the SP's entity id + $spEntityId = "SP entity ID not available"; + if (! empty($state['saml:sp:State']['SPMetadata']['entityid'])) { + $spEntityId = $state['saml:sp:State']['SPMetadata']['entityid']; + } + + $sdkConfig = [ + 'region' => $this->dynamoRegion, + 'version' => 'latest', + 'credentials' => [ + 'key' => $awsKey, + 'secret' => $awsSecret, + ], + ]; + + if (!empty($this->dynamoEndpoint)) { + $sdkConfig['endpoint'] = $this->dynamoEndpoint; + } + + $sdk = new \Aws\Sdk($sdkConfig); + + $dynamodb = $sdk->createDynamoDb(); + $marshaler = new Marshaler(); + + $userAttributes = $this->getUserAttributes($state); + + $logContents = array_merge( + $userAttributes, + [ + "ID" => uniqid(), + "IDP" => $this->getIdp($state), + "SP" => $spEntityId, + "Time" => date("Y-m-d H:i:s"), + "ExpiresAt" => time() + self::SECONDS_PER_YEAR, + ] + ); + + $logJson = json_encode($logContents); + + $item = $marshaler->marshalJson($logJson); + + $params = [ + 'TableName' => $this->dynamoLogTable, + 'Item' => $item, + ]; + + try { + $result = $dynamodb->putItem($params); + } catch (\Exception $e) { + \SimpleSAML\Logger::error("Unable to add item: ". $e->getMessage()); + } + } + + private function configsAreValid() { + $msg = ' config value not provided to LogUser.'; + + if (empty($this->dynamoRegion)) { + \SimpleSAML\Logger::error(self::DYNAMO_REGION_KEY . $msg); + return false; + } + + if (empty($this->dynamoLogTable)) { + \SimpleSAML\Logger::error(self::DYNAMO_LOG_TABLE_KEY . $msg); + return false; + } + + return true; + } + + private function getIdp(&$state) { + if (empty($state[self::IDP_KEY])) { + return 'No IDP available'; + } + + $samlIDP = $state[self::IDP_KEY]; + + // Get the potential IDPs from idp remote metadata + $metadataPath = __DIR__ . '/../../../../../metadata'; + + // If a unit test sends a different metadataPath, use it + if (isset($state['metadataPath'])) { + $metadataPath = $state['metadataPath']; + } + $idpEntries = \Sil\SspUtils\Metadata::getIdpMetadataEntries($metadataPath); + + // Get the IDPNamespace or else just use the IDP's entity ID + $idpEntry = $idpEntries[$samlIDP]; + + // If the IDPNamespace entry is a string, use it + if (isset($idpEntry[self::IDP_CODE_KEY]) && is_string($idpEntry[self::IDP_CODE_KEY])) { + return $idpEntry[self::IDP_CODE_KEY]; + } + + // Default, use the idp's entity ID + return $samlIDP; + } + + // Get the current user's common name attribute and/or eduPersonPrincipalName and/or employeeNumber + private function getUserAttributes($state) { + $attributes = $state['Attributes']; + + $cn = $this->getAttributeFrom($attributes, 'urn:oid:2.5.4.3', 'cn'); + + $eduPersonPrincipalName = $this->getAttributeFrom( + $attributes, + 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', + 'eduPersonPrincipalName' + ); + + $employeeNumber = $this->getAttributeFrom( + $attributes, + 'urn:oid:2.16.840.1.113730.3.1.3', + 'employeeNumber' + ); + + $userAttrs = []; + + $userAttrs = $this->addUserAttribute($userAttrs, "CN", $cn); + $userAttrs = $this->addUserAttribute($userAttrs, "EduPersonPrincipalName", $eduPersonPrincipalName); + $userAttrs = $this->addUserAttribute($userAttrs, "EmployeeNumber", $employeeNumber); + + return $userAttrs; + } + + private function getAttributeFrom($attributes, $oidKey, $friendlyKey) { + if (!empty($attributes[$oidKey])) { + return $attributes[$oidKey][0]; + } + + if (!empty($attributes[$friendlyKey])) { + return $attributes[$friendlyKey][0]; + } + + return ''; + } + + // Dynamodb seems to complain when a value is an empty string. + // This ensures that only attributes with a non empty value get included. + private function addUserAttribute($attributes, $attrKey, $attr) { + if (!empty($attr)) { + $attributes[$attrKey] = $attr; + } + + return $attributes; + } + +} diff --git a/modules/sildisco/lib/Auth/Process/TagGroup.php b/modules/sildisco/lib/Auth/Process/TagGroup.php new file mode 100644 index 00000000..59402907 --- /dev/null +++ b/modules/sildisco/lib/Auth/Process/TagGroup.php @@ -0,0 +1,90 @@ +getData($sessionDataType, $sessionKey); + if ( ! $sessionValue) { + $sessionValue = []; + } + + // Will we need to wrap the idp in htmlspecialchars() + $authIdps = $session->getAuthData("hub-discovery", "saml:AuthenticatingAuthority"); + + if ( ! in_array($authIdps[0], $sessionValue)) { + $sessionValue[$authIdps[0]] = $authIdps[0]; + } + + $session->setData($sessionDataType, $sessionKey, $sessionValue); + } + + +} diff --git a/modules/sildisco/lib/Auth/Source/SP.php b/modules/sildisco/lib/Auth/Source/SP.php new file mode 100644 index 00000000..b9781797 --- /dev/null +++ b/modules/sildisco/lib/Auth/Source/SP.php @@ -0,0 +1,1240 @@ +getMetadataURL(); + } + + /* For compatibility with code that assumes that $metadata->getString('entityid') + * gives the entity id. */ + $config['entityid'] = $config['entityID']; + + $this->metadata = Configuration::loadFromArray( + $config, + 'authsources[' . var_export($this->authId, true) . ']' + ); + $this->entityId = $this->metadata->getString('entityID'); + $this->idp = $this->metadata->getString('idp', null); + $this->discoURL = $this->metadata->getString('discoURL', null); + $this->disable_scoping = $this->metadata->getBoolean('disable_scoping', false); + + if (empty($this->discoURL) && Module::isModuleEnabled('discojuice')) { + $this->discoURL = Module::getModuleURL('discojuice/central.php'); + } + } + + + /** + * Retrieve the URL to the metadata of this SP. + * + * @return string The metadata URL. + */ + public function getMetadataURL() + { + return Module::getModuleURL('saml/sp/metadata.php/' . urlencode($this->authId)); + } + + + /** + * Retrieve the entity id of this SP. + * + * @return string The entity id of this SP. + */ + public function getEntityId() + { + return $this->entityId; + } + + + /** + * Retrieve the metadata array of this SP, as a remote IdP would see it. + * + * @return array The metadata array for its use by a remote IdP. + */ + public function getHostedMetadata() + { + $entityid = $this->getEntityId(); + $metadata = [ + 'entityid' => $entityid, + 'metadata-set' => 'saml20-sp-remote', + 'SingleLogoutService' => $this->getSLOEndpoints(), + 'AssertionConsumerService' => $this->getACSEndpoints(), + ]; + + // add NameIDPolicy + if ($this->metadata->hasValue('NameIDPolicy')) { + $format = $this->metadata->getValue('NameIDPolicy'); + if (is_array($format)) { + $metadata['NameIDFormat'] = Configuration::loadFromArray($format)->getString( + 'Format', + Constants::NAMEID_TRANSIENT + ); + } elseif (is_string($format)) { + $metadata['NameIDFormat'] = $format; + } + } + + // add attributes + $name = $this->metadata->getLocalizedString('name', null); + $attributes = $this->metadata->getArray('attributes', []); + if ($name !== null) { + if (!empty($attributes)) { + $metadata['name'] = $name; + $metadata['attributes'] = $attributes; + if ($this->metadata->hasValue('attributes.required')) { + $metadata['attributes.required'] = $this->metadata->getArray('attributes.required'); + } + if ($this->metadata->hasValue('description')) { + $metadata['description'] = $this->metadata->getArray('description'); + } + if ($this->metadata->hasValue('attributes.NameFormat')) { + $metadata['attributes.NameFormat'] = $this->metadata->getString('attributes.NameFormat'); + } + if ($this->metadata->hasValue('attributes.index')) { + $metadata['attributes.index'] = $this->metadata->getInteger('attributes.index'); + } + if ($this->metadata->hasValue('attributes.isDefault')) { + $metadata['attributes.isDefault'] = $this->metadata->getBoolean('attributes.isDefault'); + } + } + } + + // add organization info + $org = $this->metadata->getLocalizedString('OrganizationName', null); + if ($org !== null) { + $metadata['OrganizationName'] = $org; + $metadata['OrganizationDisplayName'] = $this->metadata->getLocalizedString('OrganizationDisplayName', $org); + $metadata['OrganizationURL'] = $this->metadata->getLocalizedString('OrganizationURL', null); + if ($metadata['OrganizationURL'] === null) { + throw new Error\Exception( + 'If OrganizationName is set, OrganizationURL must also be set.' + ); + } + } + + // add contacts + $contacts = $this->metadata->getArray('contacts', []); + foreach ($contacts as $contact) { + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + + // add technical contact + $globalConfig = Configuration::getInstance(); + $email = $globalConfig->getString('technicalcontact_email', 'na@example.org'); + if ($email && $email !== 'na@example.org') { + $contact = [ + 'emailAddress' => $email, + 'name' => $globalConfig->getString('technicalcontact_name', null), + 'contactType' => 'technical', + ]; + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + + // add certificate(s) + $certInfo = Utils\Crypto::loadPublicKey($this->metadata, false, 'new_'); + $hasNewCert = false; + if ($certInfo !== null && array_key_exists('certData', $certInfo)) { + $hasNewCert = true; + $metadata['keys'][] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => true, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => 'new_', + 'url' => Module::getModuleURL( + 'admin/federation/cert', + [ + 'set' => 'saml20-sp-hosted', + 'source' => $this->getAuthId(), + 'prefix' => 'new_' + ] + ), + 'name' => 'sp', + ]; + } + + $certInfo = Utils\Crypto::loadPublicKey($this->metadata); + if ($certInfo !== null && array_key_exists('certData', $certInfo)) { + $metadata['keys'][] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => $hasNewCert ? false : true, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => '', + 'url' => Module::getModuleURL( + 'admin/federation/cert', + [ + 'set' => 'saml20-sp-hosted', + 'source' => $this->getAuthId(), + 'prefix' => '' + ] + ), + 'name' => 'sp', + ]; + } + + // add EntityAttributes extension + if ($this->metadata->hasValue('EntityAttributes')) { + $metadata['EntityAttributes'] = $this->metadata->getArray('EntityAttributes'); + } + + // add UIInfo extension + if ($this->metadata->hasValue('UIInfo')) { + $metadata['UIInfo'] = $this->metadata->getArray('UIInfo'); + } + + // add RegistrationInfo extension + if ($this->metadata->hasValue('RegistrationInfo')) { + $metadata['RegistrationInfo'] = $this->metadata->getArray('RegistrationInfo'); + } + + // add signature options + if ($this->metadata->hasValue('WantAssertionsSigned')) { + $metadata['saml20.sign.assertion'] = $this->metadata->getBoolean('WantAssertionsSigned'); + } + if ($this->metadata->hasValue('redirect.sign')) { + $metadata['redirect.validate'] = $this->metadata->getBoolean('redirect.sign'); + } elseif ($this->metadata->hasValue('sign.authnrequest')) { + $metadata['validate.authnrequest'] = $this->metadata->getBoolean('sign.authnrequest'); + } + + return $metadata; + } + + + /** + * Retrieve the metadata of an IdP. + * + * @param string $entityId The entity id of the IdP. + * @return \SimpleSAML\Configuration The metadata of the IdP. + */ + public function getIdPMetadata($entityId) + { + assert(is_string($entityId)); + + if ($this->idp !== null && $this->idp !== $entityId) { + throw new Error\Exception('Cannot retrieve metadata for IdP ' . + var_export($entityId, true) . ' because it isn\'t a valid IdP for this SP.'); + } + + $metadataHandler = MetaDataStorageHandler::getMetadataHandler(); + + // First, look in saml20-idp-remote. + try { + return $metadataHandler->getMetaDataConfig($entityId, 'saml20-idp-remote'); + } catch (\Exception $e) { + // Metadata wasn't found + Logger::debug('getIdpMetadata: ' . $e->getMessage()); + } + + // Not found in saml20-idp-remote, look in shib13-idp-remote + try { + return $metadataHandler->getMetaDataConfig($entityId, 'shib13-idp-remote'); + } catch (\Exception $e) { + // Metadata wasn't found + Logger::debug('getIdpMetadata: ' . $e->getMessage()); + } + + // Not found + throw new Error\Exception('Could not find the metadata of an IdP with entity ID ' . + var_export($entityId, true)); + } + + + /** + * Retrieve the metadata of this SP. + * + * @return \SimpleSAML\Configuration The metadata of this SP. + */ + public function getMetadata() + { + return $this->metadata; + } + + + /** + * Get a list with the protocols supported by this SP. + * + * @return array + */ + public function getSupportedProtocols() + { + return $this->protocols; + } + + + /** + * Get the AssertionConsumerService endpoints for a given local SP. + * + * @return array + * @throws \Exception + */ + private function getACSEndpoints(): array + { + // If a list of endpoints is specified in config, take that at face value + if ($this->metadata->hasValue('AssertionConsumerService')) { + return $this->metadata->getArray('AssertionConsumerService'); + } + + $endpoints = []; + $default = [ + Constants::BINDING_HTTP_POST, + 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', + Constants::BINDING_HTTP_ARTIFACT, + 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01', + ]; + if ($this->metadata->getString('ProtocolBinding', '') === Constants::BINDING_HOK_SSO) { + $default[] = Constants::BINDING_HOK_SSO; + } + + $bindings = $this->metadata->getArray('acs.Bindings', $default); + $index = 0; + foreach ($bindings as $service) { + switch ($service) { + case Constants::BINDING_HTTP_POST: + $acs = [ + 'Binding' => Constants::BINDING_HTTP_POST, + 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + ]; + if (!in_array(Constants::NS_SAMLP, $this->protocols, true)) { + $this->protocols[] = Constants::NS_SAMLP; + } + break; + case 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post': + $acs = [ + 'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', + 'Location' => Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->getAuthId()), + ]; + if (!in_array('urn:oasis:names:tc:SAML:1.0:profiles:browser-post', $this->protocols, true)) { + $this->protocols[] = 'urn:oasis:names:tc:SAML:1.1:protocol'; + } + break; + case Constants::BINDING_HTTP_ARTIFACT: + $acs = [ + 'Binding' => Constants::BINDING_HTTP_ARTIFACT, + 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + ]; + if (!in_array(Constants::NS_SAMLP, $this->protocols, true)) { + $this->protocols[] = Constants::NS_SAMLP; + } + break; + case 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01': + $acs = [ + 'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01', + 'Location' => Module::getModuleURL( + 'saml/sp/saml1-acs.php/' . $this->getAuthId() . '/artifact' + ), + ]; + if (!in_array('urn:oasis:names:tc:SAML:1.1:protocol', $this->protocols, true)) { + $this->protocols[] = 'urn:oasis:names:tc:SAML:1.1:protocol'; + } + break; + case Constants::BINDING_HOK_SSO: + $acs = [ + 'Binding' => Constants::BINDING_HOK_SSO, + 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT, + ]; + if (!in_array(Constants::NS_SAMLP, $this->protocols, true)) { + $this->protocols[] = Constants::NS_SAMLP; + } + break; + default: + $acs = []; + } + $acs['index'] = $index; + $endpoints[] = $acs; + $index++; + } + return $endpoints; + } + + + /** + * Get the SingleLogoutService endpoints available for a given local SP. + * + * @return array + * @throws \SimpleSAML\Error\CriticalConfigurationError + */ + private function getSLOEndpoints(): array + { + $store = Store::getInstance(); + $bindings = $this->metadata->getArray( + 'SingleLogoutServiceBinding', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_SOAP, + ] + ); + $defaultLocation = Module::getModuleURL('saml/sp/saml2-logout.php/' . $this->getAuthId()); + $location = $this->metadata->getString('SingleLogoutServiceLocation', $defaultLocation); + + $endpoints = []; + foreach ($bindings as $binding) { + if ($binding == Constants::BINDING_SOAP && !($store instanceof Store\SQL)) { + // we cannot properly support SOAP logout + continue; + } + $endpoints[] = [ + 'Binding' => $binding, + 'Location' => $location, + ]; + } + return $endpoints; + } + + + /** + * Send a SAML1 SSO request to an IdP. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param array $state The state array for the current authentication. + * @return void + * @deprecated will be removed in a future version + */ + private function startSSO1(Configuration $idpMetadata, array $state): void + { + $idpEntityId = $idpMetadata->getString('entityid'); + + $state['saml:idp'] = $idpEntityId; + + $ar = new Shib13\AuthnRequest(); + $ar->setIssuer($this->entityId); + + $id = Auth\State::saveState($state, 'saml:sp:sso'); + $ar->setRelayState($id); + + $useArtifact = $idpMetadata->getBoolean('saml1.useartifact', null); + if ($useArtifact === null) { + $useArtifact = $this->metadata->getBoolean('saml1.useartifact', false); + } + + if ($useArtifact) { + $shire = Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId . '/artifact'); + } else { + $shire = Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId); + } + + $url = $ar->createRedirect($idpEntityId, $shire); + + Logger::debug('Starting SAML 1 SSO to ' . var_export($idpEntityId, true) . + ' from ' . var_export($this->entityId, true) . '.'); + Utils\HTTP::redirectTrustedURL($url); + } + + + /** + * Send a SAML2 SSO request to an IdP + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param array $state The state array for the current authentication. + * @return void + */ + private function startSSO2(Configuration $idpMetadata, array $state): void + { + if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] < 0) { + Auth\State::throwException( + $state, + new Module\saml\Error\ProxyCountExceeded(Constants::STATUS_RESPONDER) + ); + } + + $ar = Module\saml\Message::buildAuthnRequest($this->metadata, $idpMetadata); + + // GTIS + $ar->setAssertionConsumerServiceURL(Module::getModuleURL('sildisco/sp/saml2-acs.php/'.$this->authId)); + + if (isset($state['\SimpleSAML\Auth\Source.ReturnURL'])) { + $ar->setRelayState($state['\SimpleSAML\Auth\Source.ReturnURL']); + } + + $accr = null; + if ($idpMetadata->getString('AuthnContextClassRef', false)) { + $accr = Utils\Arrays::arrayize($idpMetadata->getString('AuthnContextClassRef')); + } elseif (isset($state['saml:AuthnContextClassRef'])) { + $accr = Utils\Arrays::arrayize($state['saml:AuthnContextClassRef']); + } + + if ($accr !== null) { + $comp = Constants::COMPARISON_EXACT; + if ($idpMetadata->getString('AuthnContextComparison', false)) { + $comp = $idpMetadata->getString('AuthnContextComparison'); + } elseif ( + isset($state['saml:AuthnContextComparison']) + && in_array($state['saml:AuthnContextComparison'], [ + Constants::COMPARISON_EXACT, + Constants::COMPARISON_MINIMUM, + Constants::COMPARISON_MAXIMUM, + Constants::COMPARISON_BETTER, + ], true) + ) { + $comp = $state['saml:AuthnContextComparison']; + } + $ar->setRequestedAuthnContext(['AuthnContextClassRef' => $accr, 'Comparison' => $comp]); + } + + if (isset($state['saml:Audience'])) { + $ar->setAudiences($state['saml:Audience']); + } + if (isset($state['ForceAuthn'])) { + $ar->setForceAuthn((bool) $state['ForceAuthn']); + } + + if (isset($state['isPassive'])) { + $ar->setIsPassive((bool) $state['isPassive']); + } + + if (isset($state['saml:NameID'])) { + if (!is_array($state['saml:NameID']) && !is_a($state['saml:NameID'], NameID::class)) { + throw new Error\Exception('Invalid value of $state[\'saml:NameID\'].'); + } + + $nameId = $state['saml:NameID']; + if (is_array($nameId)) { + // Must be an array > convert to object + + $nid = new NameID(); + if (!array_key_exists('Value', $nameId)) { + throw new \InvalidArgumentException('Missing "Value" in array, cannot create NameID from it.'); + } + + $nid->setValue($nameId['Value']); + if (array_key_exists('NameQualifier', $nameId) && $nameId['NameQualifier'] !== null) { + $nid->setNameQualifier($nameId['NameQualifier']); + } + if (array_key_exists('SPNameQualifier', $nameId) && $nameId['SPNameQualifier'] !== null) { + $nid->setSPNameQualifier($nameId['SPNameQualifier']); + } + if (array_key_exists('SPProvidedID', $nameId) && $nameId['SPProvidedId'] !== null) { + $nid->setSPProvidedID($nameId['SPProvidedID']); + } + if (array_key_exists('Format', $nameId) && $nameId['Format'] !== null) { + $nid->setFormat($nameId['Format']); + } + } else { + $nid = $nameId; + } + + $ar->setNameId($nid); + } + + if (isset($state['saml:NameIDPolicy'])) { + $policy = null; + if (is_string($state['saml:NameIDPolicy'])) { + $policy = [ + 'Format' => (string) $state['saml:NameIDPolicy'], + 'AllowCreate' => true, + ]; + } elseif (is_array($state['saml:NameIDPolicy'])) { + $policy = $state['saml:NameIDPolicy']; + } elseif ($state['saml:NameIDPolicy'] === null) { + $policy = ['Format' => Constants::NAMEID_TRANSIENT]; + } + if ($policy !== null) { + $ar->setNameIdPolicy($policy); + } + } + + $IDPList = []; + $requesterID = []; + + /* Only check for real info for Scoping element if we are going to send Scoping element */ + if ($this->disable_scoping !== true && $idpMetadata->getBoolean('disable_scoping', false) !== true) { + if (isset($state['saml:IDPList'])) { + $IDPList = $state['saml:IDPList']; + } + + if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] !== null) { + $ar->setProxyCount($state['saml:ProxyCount']); + } elseif ($idpMetadata->getInteger('ProxyCount', null) !== null) { + $ar->setProxyCount($idpMetadata->getInteger('ProxyCount', null)); + } elseif ($this->metadata->getInteger('ProxyCount', null) !== null) { + $ar->setProxyCount($this->metadata->getInteger('ProxyCount', null)); + } + + $requesterID = []; + if (isset($state['saml:RequesterID'])) { + $requesterID = $state['saml:RequesterID']; + } + + if (isset($state['core:SP'])) { + $requesterID[] = $state['core:SP']; + } + } else { + Logger::debug('Disabling samlp:Scoping for ' . var_export($idpMetadata->getString('entityid'), true)); + } + + $ar->setIDPList( + array_unique( + array_merge( + $this->metadata->getArray('IDPList', []), + $idpMetadata->getArray('IDPList', []), + (array) $IDPList + ) + ) + ); + + $ar->setRequesterID($requesterID); + + // If the downstream SP has set extensions then use them. + // Otherwise use extensions that might be defined in the local SP (only makes sense in a proxy scenario) + if (isset($state['saml:Extensions']) && count($state['saml:Extensions']) > 0) { + $ar->setExtensions($state['saml:Extensions']); + } else if ($this->metadata->getArray('saml:Extensions', null) !== null) { + $ar->setExtensions($this->metadata->getArray('saml:Extensions')); + } + + $providerName = $this->metadata->getString("ProviderName", null); + if ($providerName !== null) { + $ar->setProviderName($providerName); + } + + + // save IdP entity ID as part of the state + $state['ExpectedIssuer'] = $idpMetadata->getString('entityid'); + + $id = Auth\State::saveState($state, 'saml:sp:sso', true); + $ar->setId($id); + + Logger::debug( + 'Sending SAML 2 AuthnRequest to ' . var_export($idpMetadata->getString('entityid'), true) + ); + + // Select appropriate SSO endpoint + if ($ar->getProtocolBinding() === Constants::BINDING_HOK_SSO) { + /** @var array $dst */ + $dst = $idpMetadata->getDefaultEndpoint( + 'SingleSignOnService', + [ + Constants::BINDING_HOK_SSO + ] + ); + } else { + /** @var array $dst */ + $dst = $idpMetadata->getEndpointPrioritizedByBinding( + 'SingleSignOnService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST, + ] + ); + } + $ar->setDestination($dst['Location']); + + $b = Binding::getBinding($dst['Binding']); + + $this->sendSAML2AuthnRequest($state, $b, $ar); + + assert(false); + } + + + /** + * Function to actually send the authentication request. + * + * This function does not return. + * + * @param array &$state The state array. + * @param \SAML2\Binding $binding The binding. + * @param \SAML2\AuthnRequest $ar The authentication request. + * @return void + */ + public function sendSAML2AuthnRequest(array &$state, Binding $binding, AuthnRequest $ar) + { + $binding->send($ar); + assert(false); + } + + + /** + * Send a SSO request to an IdP. + * + * @param string $idp The entity ID of the IdP. + * @param array $state The state array for the current authentication. + * @return void + */ + public function startSSO($idp, array $state) + { + assert(is_string($idp)); + + $idpMetadata = $this->getIdPMetadata($idp); + + $type = $idpMetadata->getString('metadata-set'); + switch ($type) { + case 'shib13-idp-remote': + $this->startSSO1($idpMetadata, $state); + assert(false); // Should not return + case 'saml20-idp-remote': + $this->startSSO2($idpMetadata, $state); + assert(false); // Should not return + default: + // Should only be one of the known types + assert(false); + } + } + + + /** + * Start an IdP discovery service operation. + * + * @param array $state The state array. + * @return void + */ + private function startDisco(array $state): void + { + $id = Auth\State::saveState($state, 'saml:sp:sso'); + + $discoURL = $this->discoURL; + if ($discoURL === null) { + // Fallback to internal discovery service + $discoURL = Module::getModuleURL('saml/disco.php'); + } + + $returnTo = Module::getModuleURL('sildisco/sp/discoresp.php', ['AuthID' => $id]); // GTIS + + $params = [ + 'entityID' => $this->entityId, + 'return' => $returnTo, + 'returnIDParam' => 'idpentityid' + ]; + + if (isset($state['saml:IDPList'])) { + $params['IDPList'] = $state['saml:IDPList']; + } + + if (isset($state['isPassive']) && $state['isPassive']) { + $params['isPassive'] = 'true'; + } + + Utils\HTTP::redirectTrustedURL($discoURL, $params); + } + + + /** + * Start login. + * + * This function saves the information about the login, and redirects to the IdP. + * + * @param array &$state Information about the current authentication. + * @return void + */ + public function authenticate(&$state) + { + assert(is_array($state)); + + // We are going to need the authId in order to retrieve this authentication source later + $state['saml:sp:AuthId'] = $this->authId; + + $idp = $this->idp; + + if (isset($state['saml:idp'])) { + $idp = (string) $state['saml:idp']; + } + + if (isset($state['saml:IDPList']) && sizeof($state['saml:IDPList']) > 0) { + // we have a SAML IDPList (we are a proxy): filter the list of IdPs available + $mdh = MetaDataStorageHandler::getMetadataHandler(); + $matchedEntities = $mdh->getMetaDataForEntities($state['saml:IDPList'], 'saml20-idp-remote'); + + if (empty($matchedEntities)) { + // all requested IdPs are unknown + throw new Module\saml\Error\NoSupportedIDP( + Constants::STATUS_REQUESTER, + 'None of the IdPs requested are supported by this proxy.' + ); + } + + if (!is_null($idp) && !array_key_exists($idp, $matchedEntities)) { + // the IdP is enforced but not in the IDPList + throw new Module\saml\Error\NoAvailableIDP( + Constants::STATUS_REQUESTER, + 'None of the IdPs requested are available to this proxy.' + ); + } + + if (is_null($idp) && sizeof($matchedEntities) === 1) { + // only one IdP requested or valid + $idp = key($matchedEntities); + } + } + + if ($idp === null) { + $this->startDisco($state); + assert(false); + } + + $this->startSSO($idp, $state); + assert(false); + } + + + /** + * Re-authenticate an user. + * + * This function is called by the IdP to give the authentication source a chance to + * interact with the user even in the case when the user is already authenticated. + * + * @param array &$state Information about the current authentication. + * @return void + */ + public function reauthenticate(array &$state) + { + $session = Session::getSessionFromRequest(); + $data = $session->getAuthState($this->authId); + $data = $session->getAuthState($this->authId); + if ($data === null) { + throw new Error\NoState(); + } + + foreach ($data as $k => $v) { + $state[$k] = $v; + } + + /* + * GTIS + * If this SP is allowed to use more than one IdP, then send to discovery page + */ + $metadataPath = __DIR__ . '/../../../../../metadata'; + + $spEntityId = $state['SPMetadata']['entityid']; + $IDPList = array_keys(DiscoUtils::getIdpsForSp($spEntityId, $metadataPath)); + + if (sizeof($IDPList) > 1) { + $state['LoginCompletedHandler'] = array(SP::class, 'reauthPostLogin'); + $this->authenticate($state); + assert(false); + } + + // GTIS Changed this if block to avoid logging out before authenticating + // with a new IdP + if (sizeof($IDPList) > 0 && + !in_array($state['saml:sp:IdP'], $IDPList, true)) { + /* + * The user has an existing, valid session. However, the list of IdPs + * accessible to this SP does not include the IdP from the existing + * session. + */ + + Logger::warning( + "Reauthentication is needed. The IdP '${state['saml:sp:IdP']}' is not in the IDPList ". + "accessible to this Service Provider '${state['core:SP']}'." + ); + + $state['LoginCompletedHandler'] = array(SP::class, 'reauthPostLogin'); + $this->authenticate($state); + } + // End GTIS + } + + + /** + * Ask the user to log out before being able to log in again with a + * different identity provider. Note that this method is intended for + * instances of SimpleSAMLphp running as a SAML proxy, and therefore + * acting both as an SP and an IdP at the same time. + * + * This method will never return. + * + * @param array $state The state array. + * The following keys must be defined in the array: + * - 'saml:sp:IdPMetadata': a \SimpleSAML\Configuration object containing + * the metadata of the IdP that authenticated the user in the current + * session. + * - 'saml:sp:AuthId': the identifier of the current authentication source. + * - 'core:IdP': the identifier of the local IdP. + * - 'SPMetadata': an array with the metadata of this local SP. + * + * @return void + * @throws \SimpleSAML\Error\NoPassive In case the authentication request was passive. + */ + public static function askForIdPChange(array &$state) + { + assert(array_key_exists('saml:sp:IdPMetadata', $state)); + assert(array_key_exists('saml:sp:AuthId', $state)); + assert(array_key_exists('core:IdP', $state)); + assert(array_key_exists('SPMetadata', $state)); + + if (isset($state['isPassive']) && (bool) $state['isPassive']) { + // passive request, we cannot authenticate the user + throw new Module\saml\Error\NoPassive( + Constants::STATUS_REQUESTER, + 'Reauthentication required' + ); + } + + // save the state WITHOUT a restart URL, so that we don't try an IdP-initiated login if something goes wrong + $id = Auth\State::saveState($state, 'saml:proxy:invalid_idp', true); + $url = Module::getModuleURL('saml/proxy/invalid_session.php'); + Utils\HTTP::redirectTrustedURL($url, ['AuthState' => $id]); + assert(false); + } + + + /** + * Log the user out before logging in again. + * + * This method will never return. + * + * @param array $state The state array. + * @return void + */ + public static function reauthLogout(array $state) + { + Logger::debug('Proxy: logging the user out before re-authentication.'); + + if (isset($state['Responder'])) { + $state['saml:proxy:reauthLogout:PrevResponder'] = $state['Responder']; + } + $state['Responder'] = [SP::class, 'reauthPostLogout']; + + $idp = IdP::getByState($state); + $idp->handleLogoutRequest($state, null); + assert(false); + } + + + /** + * Complete login operation after re-authenticating the user on another IdP. + * + * @param array $state The authentication state. + * @return void + */ + public static function reauthPostLogin(array $state) + { + assert(isset($state['ReturnCallback'])); + + // Update session state + $session = Session::getSessionFromRequest(); + $authId = $state['saml:sp:AuthId']; + $session->doLogin($authId, Auth\State::getPersistentAuthData($state)); + + // resume the login process + call_user_func($state['ReturnCallback'], $state); + assert(false); + } + + + /** + * Post-logout handler for re-authentication. + * + * This method will never return. + * + * @param \SimpleSAML\IdP $idp The IdP we are logging out from. + * @param array &$state The state array with the state during logout. + * @return void + */ + public static function reauthPostLogout(IdP $idp, array $state) + { + assert(isset($state['saml:sp:AuthId'])); + + Logger::debug('Proxy: logout completed.'); + + if (isset($state['saml:proxy:reauthLogout:PrevResponder'])) { + $state['Responder'] = $state['saml:proxy:reauthLogout:PrevResponder']; + } + + /** @var \SimpleSAML\Module\saml\Auth\Source\SP $sp */ + $sp = Auth\Source::getById($state['saml:sp:AuthId'], Module\saml\Auth\Source\SP::class); + + Logger::debug('Proxy: logging in again.'); + $sp->authenticate($state); + assert(false); + } + + + /** + * Start a SAML 2 logout operation. + * + * @param array $state The logout state. + * @return void + */ + public function startSLO2(&$state) + { + assert(is_array($state)); + assert(array_key_exists('saml:logout:IdP', $state)); + assert(array_key_exists('saml:logout:NameID', $state)); + assert(array_key_exists('saml:logout:SessionIndex', $state)); + + $id = Auth\State::saveState($state, 'saml:slosent'); + + $idp = $state['saml:logout:IdP']; + $nameId = $state['saml:logout:NameID']; + $sessionIndex = $state['saml:logout:SessionIndex']; + + $idpMetadata = $this->getIdPMetadata($idp); + + /** @var array $endpoint */ + $endpoint = $idpMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ], + false + ); + if ($endpoint === false) { + Logger::info('No logout endpoint for IdP ' . var_export($idp, true) . '.'); + return; + } + + $lr = Module\saml\Message::buildLogoutRequest($this->metadata, $idpMetadata); + $lr->setNameId($nameId); + $lr->setSessionIndex($sessionIndex); + $lr->setRelayState($id); + $lr->setDestination($endpoint['Location']); + + $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', null); + if ($encryptNameId === null) { + $encryptNameId = $this->metadata->getBoolean('nameid.encryption', false); + } + if ($encryptNameId) { + $lr->encryptNameId(Module\saml\Message::getEncryptionKey($idpMetadata)); + } + + $b = Binding::getBinding($endpoint['Binding']); + $b->send($lr); + + assert(false); + } + + + /** + * Start logout operation. + * + * @param array $state The logout state. + * @return void + */ + public function logout(&$state) + { + assert(is_array($state)); + assert(array_key_exists('saml:logout:Type', $state)); + + $logoutType = $state['saml:logout:Type']; + switch ($logoutType) { + case 'saml1': + // Nothing to do + return; + case 'saml2': + $this->startSLO2($state); + return; + default: + // Should never happen + assert(false); + } + } + + + /** + * Handle a response from a SSO operation. + * + * @param array $state The authentication state. + * @param string $idp The entity id of the IdP. + * @param array $attributes The attributes. + * @return void + */ + public function handleResponse(array $state, $idp, array $attributes) + { + assert(is_string($idp)); + assert(array_key_exists('LogoutState', $state)); + assert(array_key_exists('saml:logout:Type', $state['LogoutState'])); + + $idpMetadata = $this->getIdPMetadata($idp); + + $spMetadataArray = $this->metadata->toArray(); + $idpMetadataArray = $idpMetadata->toArray(); + + /* Save the IdP in the state array. */ + $state['saml:sp:IdP'] = $idp; + $state['PersistentAuthData'][] = 'saml:sp:IdP'; + + $authProcState = [ + 'saml:sp:IdP' => $idp, + 'saml:sp:State' => $state, + 'ReturnCall' => [SP::class, 'onProcessingCompleted'], + + 'Attributes' => $attributes, + 'Destination' => $spMetadataArray, + 'Source' => $idpMetadataArray, + ]; + + if (isset($state['saml:sp:NameID'])) { + $authProcState['saml:sp:NameID'] = $state['saml:sp:NameID']; + } + if (isset($state['saml:sp:SessionIndex'])) { + $authProcState['saml:sp:SessionIndex'] = $state['saml:sp:SessionIndex']; + } + + $pc = new Auth\ProcessingChain($idpMetadataArray, $spMetadataArray, 'sp'); + $pc->processState($authProcState); + + self::onProcessingCompleted($authProcState); + } + + + /** + * Handle a logout request from an IdP. + * + * @param string $idpEntityId The entity ID of the IdP. + * @return void + */ + public function handleLogout($idpEntityId) + { + assert(is_string($idpEntityId)); + + /* Call the logout callback we registered in onProcessingCompleted(). */ + $this->callLogoutCallback($idpEntityId); + } + + + /** + * Handle an unsolicited login operations. + * + * This method creates a session from the information received. It will + * then redirect to the given URL. This is used to handle IdP initiated + * SSO. This method will never return. + * + * @param string $authId The id of the authentication source that received the request. + * @param array $state A state array. + * @param string $redirectTo The URL we should redirect the user to after updating + * the session. The function will check if the URL is allowed, so there is no need to + * manually check the URL on beforehand. Please refer to the 'trusted.url.domains' + * configuration directive for more information about allowing (or disallowing) URLs. + * @return void + */ + public static function handleUnsolicitedAuth($authId, array $state, $redirectTo) + { + assert(is_string($authId)); + assert(is_string($redirectTo)); + + $session = Session::getSessionFromRequest(); + $session->doLogin($authId, Auth\State::getPersistentAuthData($state)); + + Utils\HTTP::redirectUntrustedURL($redirectTo); + } + + + /** + * Called when we have completed the procssing chain. + * + * @param array $authProcState The processing chain state. + * @return void + */ + public static function onProcessingCompleted(array $authProcState) + { + assert(array_key_exists('saml:sp:IdP', $authProcState)); + assert(array_key_exists('saml:sp:State', $authProcState)); + assert(array_key_exists('Attributes', $authProcState)); + + $idp = $authProcState['saml:sp:IdP']; + $state = $authProcState['saml:sp:State']; + + $sourceId = $state['saml:sp:AuthId']; + + /** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */ + $source = Auth\Source::getById($sourceId); + if ($source === null) { + throw new \Exception('Could not find authentication source with id ' . $sourceId); + } + + // Register a callback that we can call if we receive a logout request from the IdP + $source->addLogoutCallback($idp, $state); + + $state['Attributes'] = $authProcState['Attributes']; + + if (isset($state['saml:sp:isUnsolicited']) && (bool) $state['saml:sp:isUnsolicited']) { + if (!empty($state['saml:sp:RelayState'])) { + $redirectTo = $state['saml:sp:RelayState']; + } else { + $redirectTo = $source->getMetadata()->getString('RelayState', '/'); + } + self::handleUnsolicitedAuth($sourceId, $state, $redirectTo); + } + + Auth\Source::completeAuth($state); + } +} diff --git a/modules/sildisco/lib/IdP/SAML2.php b/modules/sildisco/lib/IdP/SAML2.php new file mode 100644 index 00000000..4d975bd6 --- /dev/null +++ b/modules/sildisco/lib/IdP/SAML2.php @@ -0,0 +1,1515 @@ +getConfig(); + + $assertion = self::buildAssertion($idpMetadata, $spMetadata, $state); + + if (isset($state['saml:AuthenticatingAuthority'])) { + $assertion->setAuthenticatingAuthority($state['saml:AuthenticatingAuthority']); + } + + // create the session association (for logout) + $association = [ + 'id' => 'saml:' . $spEntityId, + 'Handler' => '\SimpleSAML\Module\saml\IdP\SAML2', + 'Expires' => $assertion->getSessionNotOnOrAfter(), + 'saml:entityID' => $spEntityId, + 'saml:NameID' => $state['saml:idp:NameID'], + 'saml:SessionIndex' => $assertion->getSessionIndex(), + ]; + + // maybe encrypt the assertion + $assertion = self::encryptAssertion($idpMetadata, $spMetadata, $assertion); + + // create the response + $ar = self::buildResponse($idpMetadata, $spMetadata, $consumerURL); + $ar->setInResponseTo($requestId); + $ar->setRelayState($relayState); + $ar->setAssertions([$assertion]); + + // register the session association with the IdP + $idp->addAssociation($association); + + $statsData = [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'protocol' => 'saml2', + ]; + if (isset($state['saml:AuthnRequestReceivedAt'])) { + $statsData['logintime'] = microtime(true) - $state['saml:AuthnRequestReceivedAt']; + } + Stats::log('saml:idp:Response', $statsData); + + // send the response + $binding = Binding::getBinding($protocolBinding); + $binding->send($ar); + } + + + /** + * Handle authentication error. + * + * \SimpleSAML\Error\Exception $exception The exception. + * + * @param array $state The error state. + * @return void + */ + public static function handleAuthError(Error\Exception $exception, array $state) + { + assert(isset($state['SPMetadata'])); + assert(isset($state['saml:ConsumerURL'])); + assert(array_key_exists('saml:RequestId', $state)); // Can be NULL. + assert(array_key_exists('saml:RelayState', $state)); // Can be NULL. + + $spMetadata = $state["SPMetadata"]; + $spEntityId = $spMetadata['entityid']; + $spMetadata = Configuration::loadFromArray( + $spMetadata, + '$metadata[' . var_export($spEntityId, true) . ']' + ); + + $requestId = $state['saml:RequestId']; + $relayState = $state['saml:RelayState']; + $consumerURL = $state['saml:ConsumerURL']; + $protocolBinding = $state['saml:Binding']; + + $idp = IdP::getByState($state); + + $idpMetadata = $idp->getConfig(); + + $error = \SimpleSAML\Module\saml\Error::fromException($exception); + + Logger::warning("Returning error to SP with entity ID '" . var_export($spEntityId, true) . "'."); + $exception->log(Logger::WARNING); + + $ar = self::buildResponse($idpMetadata, $spMetadata, $consumerURL); + $ar->setInResponseTo($requestId); + $ar->setRelayState($relayState); + + $status = [ + 'Code' => $error->getStatus(), + 'SubCode' => $error->getSubStatus(), + 'Message' => $error->getStatusMessage(), + ]; + $ar->setStatus($status); + + $statsData = [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'protocol' => 'saml2', + 'error' => $status, + ]; + if (isset($state['saml:AuthnRequestReceivedAt'])) { + $statsData['logintime'] = microtime(true) - $state['saml:AuthnRequestReceivedAt']; + } + Stats::log('saml:idp:Response:error', $statsData); + + $binding = Binding::getBinding($protocolBinding); + $binding->send($ar); + } + + + /** + * Find SP AssertionConsumerService based on parameter in AuthnRequest. + * + * @param array $supportedBindings The bindings we allow for the response. + * @param \SimpleSAML\Configuration $spMetadata The metadata for the SP. + * @param string|null $AssertionConsumerServiceURL AssertionConsumerServiceURL from request. + * @param string|null $ProtocolBinding ProtocolBinding from request. + * @param int|null $AssertionConsumerServiceIndex AssertionConsumerServiceIndex from request. + * + * @return array|null Array with the Location and Binding we should use for the response. + */ + private static function getAssertionConsumerService( + array $supportedBindings, + Configuration $spMetadata, + string $AssertionConsumerServiceURL = null, + string $ProtocolBinding = null, + int $AssertionConsumerServiceIndex = null + ): ?array { + /* We want to pick the best matching endpoint in the case where for example + * only the ProtocolBinding is given. We therefore pick endpoints with the + * following priority: + * 1. isDefault="true" + * 2. isDefault unset + * 3. isDefault="false" + */ + $firstNotFalse = null; + $firstFalse = null; + foreach ($spMetadata->getEndpoints('AssertionConsumerService') as $ep) { + if ($AssertionConsumerServiceURL !== null && $ep['Location'] !== $AssertionConsumerServiceURL) { + continue; + } + if ($ProtocolBinding !== null && $ep['Binding'] !== $ProtocolBinding) { + continue; + } + if ($AssertionConsumerServiceIndex !== null && $ep['index'] !== $AssertionConsumerServiceIndex) { + continue; + } + + if (!in_array($ep['Binding'], $supportedBindings, true)) { + /* The endpoint has an unsupported binding. */ + continue; + } + + // we have an endpoint that matches all our requirements. Check if it is the best one + + if (array_key_exists('isDefault', $ep)) { + if ($ep['isDefault'] === true) { + // this is the first matching endpoint with isDefault set to true + return $ep; + } + // isDefault is set to FALSE, but the endpoint is still usable + if ($firstFalse === null) { + // this is the first endpoint that we can use + $firstFalse = $ep; + } + } else { + if ($firstNotFalse === null) { + // this is the first endpoint without isDefault set + $firstNotFalse = $ep; + } + } + } + + if ($firstNotFalse !== null) { + return $firstNotFalse; + } elseif ($firstFalse !== null) { + return $firstFalse; + } + + Logger::warning('Authentication request specifies invalid AssertionConsumerService:'); + if ($AssertionConsumerServiceURL !== null) { + Logger::warning('AssertionConsumerServiceURL: ' . var_export($AssertionConsumerServiceURL, true)); + } + if ($ProtocolBinding !== null) { + Logger::warning('ProtocolBinding: ' . var_export($ProtocolBinding, true)); + } + if ($AssertionConsumerServiceIndex !== null) { + Logger::warning( + 'AssertionConsumerServiceIndex: ' . var_export($AssertionConsumerServiceIndex, true) + ); + } + + // we have no good endpoints. Our last resort is to just use the default endpoint + return $spMetadata->getDefaultEndpoint('AssertionConsumerService', $supportedBindings); + } + + + /** + * Receive an authentication request. + * + * @param \SimpleSAML\IdP $idp The IdP we are receiving it for. + * @return void + * @throws \SimpleSAML\Error\BadRequest In case an error occurs when trying to receive the request. + */ + public static function receiveAuthnRequest(\SimpleSAML\IdP $idp) + { + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + + $supportedBindings = [Constants::BINDING_HTTP_POST]; + if ($idpMetadata->getBoolean('saml20.sendartifact', false)) { + $supportedBindings[] = Constants::BINDING_HTTP_ARTIFACT; + } + if ($idpMetadata->getBoolean('saml20.hok.assertion', false)) { + $supportedBindings[] = Constants::BINDING_HOK_SSO; + } + if ($idpMetadata->getBoolean('saml20.ecp', false)) { + $supportedBindings[] = Constants::BINDING_PAOS; + } + + if (isset($_REQUEST['spentityid']) || isset($_REQUEST['providerId'])) { + /* IdP initiated authentication. */ + + if (isset($_REQUEST['cookieTime'])) { + $cookieTime = (int) $_REQUEST['cookieTime']; + if ($cookieTime + 5 > time()) { + /* + * Less than five seconds has passed since we were + * here the last time. Cookies are probably disabled. + */ + Utils\HTTP::checkSessionCookie(Utils\HTTP::getSelfURL()); + } + } + + $spEntityId = (string) isset($_REQUEST['spentityid']) ? $_REQUEST['spentityid'] : $_REQUEST['providerId']; + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + if (isset($_REQUEST['RelayState'])) { + $relayState = (string) $_REQUEST['RelayState']; + } elseif (isset($_REQUEST['target'])) { + $relayState = (string) $_REQUEST['target']; + } else { + $relayState = null; + } + + if (isset($_REQUEST['binding'])) { + $protocolBinding = (string) $_REQUEST['binding']; + } else { + $protocolBinding = null; + } + + if (isset($_REQUEST['NameIDFormat'])) { + $nameIDFormat = (string) $_REQUEST['NameIDFormat']; + } else { + $nameIDFormat = null; + } + + if (isset($_REQUEST['ConsumerURL'])) { + $consumerURL = (string)$_REQUEST['ConsumerURL']; + } elseif (isset($_REQUEST['shire'])) { + $consumerURL = (string)$_REQUEST['shire']; + } else { + $consumerURL = null; + } + + $requestId = null; + $IDPList = []; + $ProxyCount = null; + $RequesterID = null; + $forceAuthn = false; + $isPassive = false; + $consumerIndex = null; + $extensions = null; + $allowCreate = true; + $authnContext = null; + + $idpInit = true; + + Logger::info( + 'SAML2.0 - IdP.SSOService: IdP initiated authentication: ' . var_export($spEntityId, true) + ); + } else { + try { + $binding = Binding::getCurrentBinding(); + } catch (Exception $e) { + header($_SERVER["SERVER_PROTOCOL"]." 405 Method Not Allowed", true, 405); + exit; + } + $request = $binding->receive(); + + if (!($request instanceof AuthnRequest)) { + throw new Error\BadRequest( + 'Message received on authentication request endpoint wasn\'t an authentication request.' + ); + } + + /** @psalm-var null|string|\SAML2\XML\saml\Issuer $issuer Remove in SSP 2.0 */ + $issuer = $request->getIssuer(); + if ($issuer === null) { + throw new Error\BadRequest( + 'Received message on authentication request endpoint without issuer.' + ); + } elseif ($issuer instanceof Issuer) { + /** @psalm-var string|null $spEntityId */ + $spEntityId = $issuer->getValue(); + if ($spEntityId === null) { + /* Without an issuer we have no way to respond to the message. */ + throw new Error\BadRequest('Received message on logout endpoint without issuer.'); + } + } else { // we got a string, old case + $spEntityId = $issuer; + } + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + \SimpleSAML\Module\saml\Message::validateMessage($spMetadata, $idpMetadata, $request); + + $relayState = $request->getRelayState(); + + $requestId = $request->getId(); + $IDPList = $request->getIDPList(); + $ProxyCount = $request->getProxyCount(); + if ($ProxyCount !== null) { + $ProxyCount--; + } + $RequesterID = $request->getRequesterID(); + $forceAuthn = $request->getForceAuthn(); + $isPassive = $request->getIsPassive(); + $consumerURL = $request->getAssertionConsumerServiceURL(); + $protocolBinding = $request->getProtocolBinding(); + $consumerIndex = $request->getAssertionConsumerServiceIndex(); + $extensions = $request->getExtensions(); + $authnContext = $request->getRequestedAuthnContext(); + + $nameIdPolicy = $request->getNameIdPolicy(); + if (isset($nameIdPolicy['Format'])) { + $nameIDFormat = $nameIdPolicy['Format']; + } else { + $nameIDFormat = null; + } + if (isset($nameIdPolicy['AllowCreate'])) { + $allowCreate = $nameIdPolicy['AllowCreate']; + } else { + $allowCreate = false; + } + + $idpInit = false; + + Logger::info( + 'SAML2.0 - IdP.SSOService: incoming authentication request: ' . var_export($spEntityId, true) + ); + } + + Stats::log('saml:idp:AuthnRequest', [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'forceAuthn' => $forceAuthn, + 'isPassive' => $isPassive, + 'protocol' => 'saml2', + 'idpInit' => $idpInit, + ]); + + $acsEndpoint = self::getAssertionConsumerService( + $supportedBindings, + $spMetadata, + $consumerURL, + $protocolBinding, + $consumerIndex + ); + if ($acsEndpoint === null) { + throw new Exception('Unable to use any of the ACS endpoints found for SP \'' . $spEntityId . '\''); + } + + $IDPList = array_unique(array_merge($IDPList, $spMetadata->getArrayizeString('IDPList', []))); + if ($ProxyCount === null) { + $ProxyCount = $spMetadata->getInteger('ProxyCount', null); + } + + if (!$forceAuthn) { + $forceAuthn = $spMetadata->getBoolean('ForceAuthn', false); + } + + $sessionLostParams = [ + 'spentityid' => $spEntityId, + ]; + if ($relayState !== null) { + $sessionLostParams['RelayState'] = $relayState; + } + /* + Putting cookieTime as the last parameter makes unit testing easier since we don't need to handle a + changing time component in the middle of the url + */ + $sessionLostParams['cookieTime'] = time(); + + $sessionLostURL = Utils\HTTP::addURLParameters( + Utils\HTTP::getSelfURLNoQuery(), + $sessionLostParams + ); + + + /* + * Added by GTIS. + * This code is intended to ensure that a session from a new SP + * will be forced to reauthenticate if that SP is not allowed + * to authenticate through any of the IDP's that have so far + * been used for authentication. + * + * In order for this for this to avoid forcing authentication + * in every case, the hub's saml20-idp-hosted.php entry needs + * to include an authproc entry that adds each authenticating + * IDP to a list in the session. + * That list should be found in ... + * sessionDataType: 'sildisco:authentication' + * sessionKey: 'authenticated_idps' + * + * Another feature is that it forces the user to the discovery page, + * if the SP is allowed to use more than one IDP. The reason for this + * is that we want the user to be able to pick which of his ID's + * to use for this session. The way it is carried out is by expiring + * the user's session on the hub. (It does not log the user out from + * any of the IDP's.) + * + */ + $session = \SimpleSAML\Session::getSessionFromRequest(); + $sessionDataType = 'sildisco:authentication'; + $spIdKey = 'spentityid'; + $session->setData($sessionDataType, $spIdKey, $spEntityId); + $metadataPath = __DIR__ . '/../../../../metadata'; + $IDPList = array_keys(DiscoUtils::getIdpsForSp($spEntityId, $metadataPath)); + if ( ! $forceAuthn ) { + $sessionDataType = 'sildisco:authentication'; + $sessionKey = 'authenticated_idps'; + $authenticatedIdps = $session->getData($sessionDataType, $sessionKey); + + if ($authenticatedIdps) { + $metadataPath = __DIR__ . '/../../../../metadata/'; + $allowedIdps = DiscoUtils::getReducedIdpList( + $authenticatedIdps, + $metadataPath, + $spEntityId + ); + if ( ! $allowedIdps) { + $IDPList = Null; + $forceAuthn = True; + } + } else { // If there are no authenticated IDPs + $forceAuthn = True; + } + } + /* + * End of GTIS addition + */ + + $state = [ + 'Responder' => ['\SimpleSAML\Module\saml\IdP\SAML2', 'sendResponse'], + Auth\State::EXCEPTION_HANDLER_FUNC => [ + '\SimpleSAML\Module\saml\IdP\SAML2', + 'handleAuthError' + ], + Auth\State::RESTART => $sessionLostURL, + + 'SPMetadata' => $spMetadata->toArray(), + 'saml:RelayState' => $relayState, + 'saml:RequestId' => $requestId, + 'saml:IDPList' => $IDPList, + 'saml:ProxyCount' => $ProxyCount, + 'saml:RequesterID' => $RequesterID, + 'ForceAuthn' => $forceAuthn, + 'isPassive' => $isPassive, + 'saml:ConsumerURL' => $acsEndpoint['Location'], + 'saml:Binding' => $acsEndpoint['Binding'], + 'saml:NameIDFormat' => $nameIDFormat, + 'saml:AllowCreate' => $allowCreate, + 'saml:Extensions' => $extensions, + 'saml:AuthnRequestReceivedAt' => microtime(true), + 'saml:RequestedAuthnContext' => $authnContext, + ]; + + $idp->handleAuthenticationRequest($state); + } + + + /** + * Send a logout request to a given association. + * + * @param \SimpleSAML\IdP $idp The IdP we are sending a logout request from. + * @param array $association The association that should be terminated. + * @param string|null $relayState An id that should be carried across the logout. + * @return void + */ + public static function sendLogoutRequest(IdP $idp, array $association, $relayState) + { + assert(is_string($relayState) || $relayState === null); + + Logger::info('Sending SAML 2.0 LogoutRequest to: ' . var_export($association['saml:entityID'], true)); + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); + + Stats::log('saml:idp:LogoutRequest:sent', [ + 'spEntityID' => $association['saml:entityID'], + 'idpEntityID' => $idpMetadata->getString('entityid'), + ]); + + /** @var array $dst */ + $dst = $spMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ] + ); + $binding = Binding::getBinding($dst['Binding']); + $lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState); + $lr->setDestination($dst['Location']); + + $binding->send($lr); + } + + + /** + * Send a logout response. + * + * @param \SimpleSAML\IdP $idp The IdP we are sending a logout request from. + * @param array &$state The logout state array. + * @return void + */ + public static function sendLogoutResponse(IdP $idp, array $state) + { + assert(isset($state['saml:SPEntityId'])); + assert(isset($state['saml:RequestId'])); + assert(array_key_exists('saml:RelayState', $state)); // Can be NULL. + + $spEntityId = $state['saml:SPEntityId']; + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + $lr = \SimpleSAML\Module\saml\Message::buildLogoutResponse($idpMetadata, $spMetadata); + $lr->setInResponseTo($state['saml:RequestId']); + $lr->setRelayState($state['saml:RelayState']); + + if (isset($state['core:Failed']) && $state['core:Failed']) { + $partial = true; + $lr->setStatus([ + 'Code' => Constants::STATUS_SUCCESS, + 'SubCode' => Constants::STATUS_PARTIAL_LOGOUT, + ]); + Logger::info('Sending logout response for partial logout to SP ' . var_export($spEntityId, true)); + } else { + $partial = false; + Logger::debug('Sending logout response to SP ' . var_export($spEntityId, true)); + } + + Stats::log('saml:idp:LogoutResponse:sent', [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'partial' => $partial + ]); + + /** @var array $dst */ + $dst = $spMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ] + ); + $binding = Binding::getBinding($dst['Binding']); + if (isset($dst['ResponseLocation'])) { + $dst = $dst['ResponseLocation']; + } else { + $dst = $dst['Location']; + } + $lr->setDestination($dst); + + $binding->send($lr); + } + + + /** + * Receive a logout message. + * + * @param \SimpleSAML\IdP $idp The IdP we are receiving it for. + * @return void + * @throws \SimpleSAML\Error\BadRequest In case an error occurs while trying to receive the logout message. + */ + public static function receiveLogoutMessage(IdP $idp) + { + $binding = Binding::getCurrentBinding(); + $message = $binding->receive(); + + /** @psalm-var null|string|\SAML2\XML\saml\Issuer Remove in SSP 2.0 */ + $issuer = $message->getIssuer(); + if ($issuer === null) { + /* Without an issuer we have no way to respond to the message. */ + throw new Error\BadRequest('Received message on logout endpoint without issuer.'); + } elseif ($issuer instanceof Issuer) { + /** @psalm-var string|null $spEntityId */ + $spEntityId = $issuer->getValue(); + if ($spEntityId === null) { + /* Without an issuer we have no way to respond to the message. */ + throw new Error\BadRequest('Received message on logout endpoint without issuer.'); + } + } else { + $spEntityId = $issuer; + } + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + \SimpleSAML\Module\saml\Message::validateMessage($spMetadata, $idpMetadata, $message); + + if ($message instanceof LogoutResponse) { + Logger::info('Received SAML 2.0 LogoutResponse from: ' . var_export($spEntityId, true)); + $statsData = [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + ]; + if (!$message->isSuccess()) { + $statsData['error'] = $message->getStatus(); + } + Stats::log('saml:idp:LogoutResponse:recv', $statsData); + + $relayState = $message->getRelayState(); + + if (!$message->isSuccess()) { + $logoutError = \SimpleSAML\Module\saml\Message::getResponseError($message); + Logger::warning('Unsuccessful logout. Status was: ' . $logoutError); + } else { + $logoutError = null; + } + + $assocId = 'saml:' . $spEntityId; + + $idp->handleLogoutResponse($assocId, $relayState, $logoutError); + } elseif ($message instanceof LogoutRequest) { + Logger::info('Received SAML 2.0 LogoutRequest from: ' . var_export($spEntityId, true)); + Stats::log('saml:idp:LogoutRequest:recv', [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + ]); + + $spStatsId = $spMetadata->getString('core:statistics-id', $spEntityId); + Logger::stats('saml20-idp-SLO spinit ' . $spStatsId . ' ' . $idpMetadata->getString('entityid')); + + $state = [ + 'Responder' => ['\SimpleSAML\Module\saml\IdP\SAML2', 'sendLogoutResponse'], + 'saml:SPEntityId' => $spEntityId, + 'saml:RelayState' => $message->getRelayState(), + 'saml:RequestId' => $message->getId(), + ]; + + $assocId = 'saml:' . $spEntityId; + $idp->handleLogoutRequest($state, $assocId); + } else { + throw new Error\BadRequest('Unknown message received on logout endpoint: ' . get_class($message)); + } + } + + + /** + * Retrieve a logout URL for a given logout association. + * + * @param \SimpleSAML\IdP $idp The IdP we are sending a logout request from. + * @param array $association The association that should be terminated. + * @param string|NULL $relayState An id that should be carried across the logout. + * + * @return string The logout URL. + */ + public static function getLogoutURL(IdP $idp, array $association, $relayState) + { + assert(is_string($relayState) || $relayState === null); + + Logger::info('Sending SAML 2.0 LogoutRequest to: ' . var_export($association['saml:entityID'], true)); + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); + + $bindings = [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ]; + + /** @var array $dst */ + $dst = $spMetadata->getEndpointPrioritizedByBinding('SingleLogoutService', $bindings); + + if ($dst['Binding'] === Constants::BINDING_HTTP_POST) { + $params = ['association' => $association['id'], 'idp' => $idp->getId()]; + if ($relayState !== null) { + $params['RelayState'] = $relayState; + } + return Module::getModuleURL('core/idp/logout-iframe-post.php', $params); + } + + $lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState); + $lr->setDestination($dst['Location']); + + $binding = new HTTPRedirect(); + return $binding->getRedirectURL($lr); + } + + + /** + * Retrieve the metadata for the given SP association. + * + * @param \SimpleSAML\IdP $idp The IdP the association belongs to. + * @param array $association The SP association. + * + * @return \SimpleSAML\Configuration Configuration object for the SP metadata. + */ + public static function getAssociationConfig(IdP $idp, array $association) + { + $metadata = MetaDataStorageHandler::getMetadataHandler(); + try { + return $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); + } catch (Exception $e) { + return Configuration::loadFromArray([], 'Unknown SAML 2 entity.'); + } + } + + + /** + * Retrieve the metadata of a hosted SAML 2 IdP. + * + * @param string $entityid The entity ID of the hosted SAML 2 IdP whose metadata we want. + * + * @return array + * @throws \SimpleSAML\Error\CriticalConfigurationError + * @throws \SimpleSAML\Error\Exception + * @throws \SimpleSAML\Error\MetadataNotFound + */ + public static function getHostedMetadata($entityid) + { + $handler = MetaDataStorageHandler::getMetadataHandler(); + $config = $handler->getMetaDataConfig($entityid, 'saml20-idp-hosted'); + + // configure endpoints + $ssob = $handler->getGenerated('SingleSignOnServiceBinding', 'saml20-idp-hosted'); + $slob = $handler->getGenerated('SingleLogoutServiceBinding', 'saml20-idp-hosted'); + $ssol = $handler->getGenerated('SingleSignOnService', 'saml20-idp-hosted'); + $slol = $handler->getGenerated('SingleLogoutService', 'saml20-idp-hosted'); + + $sso = []; + if (is_array($ssob)) { + foreach ($ssob as $binding) { + $sso[] = [ + 'Binding' => $binding, + 'Location' => $ssol, + ]; + } + } else { + $sso[] = [ + 'Binding' => $ssob, + 'Location' => $ssol, + ]; + } + + $slo = []; + if (is_array($slob)) { + foreach ($slob as $binding) { + $slo[] = [ + 'Binding' => $binding, + 'Location' => $slol, + ]; + } + } else { + $slo[] = [ + 'Binding' => $slob, + 'Location' => $slol, + ]; + } + + $metadata = [ + 'metadata-set' => 'saml20-idp-hosted', + 'entityid' => $entityid, + 'SingleSignOnService' => $sso, + 'SingleLogoutService' => $slo, + 'NameIDFormat' => $config->getArrayizeString('NameIDFormat', Constants::NAMEID_TRANSIENT), + ]; + + // add certificates + $keys = []; + $certInfo = Utils\Crypto::loadPublicKey($config, false, 'new_'); + $hasNewCert = false; + if ($certInfo !== null) { + $keys[] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => true, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => 'new_', + ]; + $hasNewCert = true; + } + + /** @var array $certInfo */ + $certInfo = Utils\Crypto::loadPublicKey($config, true); + $keys[] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => $hasNewCert === false, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => '', + ]; + + if ($config->hasValue('https.certificate')) { + /** @var array $httpsCert */ + $httpsCert = Utils\Crypto::loadPublicKey($config, true, 'https.'); + $keys[] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => false, + 'X509Certificate' => $httpsCert['certData'], + 'prefix' => 'https.' + ]; + } + $metadata['keys'] = $keys; + + // add ArtifactResolutionService endpoint, if enabled + if ($config->getBoolean('saml20.sendartifact', false)) { + $metadata['ArtifactResolutionService'][] = [ + 'index' => 0, + 'Binding' => Constants::BINDING_SOAP, + 'Location' => Utils\HTTP::getBaseURL() . 'saml2/idp/ArtifactResolutionService.php' + ]; + } + + // add Holder of Key, if enabled + if ($config->getBoolean('saml20.hok.assertion', false)) { + array_unshift( + $metadata['SingleSignOnService'], + [ + 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => Constants::BINDING_HOK_SSO, + 'Location' => Utils\HTTP::getBaseURL() . 'saml2/idp/SSOService.php', + ] + ); + } + + // add ECP profile, if enabled + if ($config->getBoolean('saml20.ecp', false)) { + $metadata['SingleSignOnService'][] = [ + 'index' => 0, + 'Binding' => Constants::BINDING_SOAP, + 'Location' => Utils\HTTP::getBaseURL() . 'saml2/idp/SSOService.php', + ]; + } + + // add organization information + if ($config->hasValue('OrganizationName')) { + $metadata['OrganizationName'] = $config->getLocalizedString('OrganizationName'); + $metadata['OrganizationDisplayName'] = $config->getLocalizedString( + 'OrganizationDisplayName', + $metadata['OrganizationName'] + ); + + if (!$config->hasValue('OrganizationURL')) { + throw new Error\Exception('If OrganizationName is set, OrganizationURL must also be set.'); + } + $metadata['OrganizationURL'] = $config->getLocalizedString('OrganizationURL'); + } + + // add scope + if ($config->hasValue('scope')) { + $metadata['scope'] = $config->getArray('scope'); + } + + // add extensions + if ($config->hasValue('EntityAttributes')) { + $metadata['EntityAttributes'] = $config->getArray('EntityAttributes'); + + // check for entity categories + if (Utils\Config\Metadata::isHiddenFromDiscovery($metadata)) { + $metadata['hide.from.discovery'] = true; + } + } + + if ($config->hasValue('UIInfo')) { + $metadata['UIInfo'] = $config->getArray('UIInfo'); + } + + if ($config->hasValue('DiscoHints')) { + $metadata['DiscoHints'] = $config->getArray('DiscoHints'); + } + + if ($config->hasValue('RegistrationInfo')) { + $metadata['RegistrationInfo'] = $config->getArray('RegistrationInfo'); + } + + // configure signature options + if ($config->hasValue('validate.authnrequest')) { + $metadata['sign.authnrequest'] = $config->getBoolean('validate.authnrequest'); + } + + if ($config->hasValue('redirect.validate')) { + $metadata['redirect.sign'] = $config->getBoolean('redirect.validate'); + } + + // add contact information + if ($config->hasValue('contacts')) { + $contacts = $config->getArray('contacts'); + foreach ($contacts as $contact) { + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + } + + $globalConfig = Configuration::getInstance(); + $email = $globalConfig->getString('technicalcontact_email', false); + if ($email && $email !== 'na@example.org') { + $contact = [ + 'emailAddress' => $email, + 'name' => $globalConfig->getString('technicalcontact_name', null), + 'contactType' => 'technical', + ]; + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + + return $metadata; + } + + + /** + * Calculate the NameID value that should be used. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array &$state The authentication state of the user. + * + * @return string|null The NameID value. + */ + private static function generateNameIdValue( + Configuration $idpMetadata, + Configuration $spMetadata, + array &$state + ): ?string { + $attribute = $spMetadata->getString('simplesaml.nameidattribute', null); + if ($attribute === null) { + $attribute = $idpMetadata->getString('simplesaml.nameidattribute', null); + if ($attribute === null) { + if (!isset($state['UserID'])) { + Logger::error('Unable to generate NameID. Check the userid.attribute option.'); + return null; + } + $attributeValue = $state['UserID']; + $idpEntityId = $idpMetadata->getString('entityid'); + $spEntityId = $spMetadata->getString('entityid'); + + $secretSalt = Utils\Config::getSecretSalt(); + + $uidData = 'uidhashbase' . $secretSalt; + $uidData .= strlen($idpEntityId) . ':' . $idpEntityId; + $uidData .= strlen($spEntityId) . ':' . $spEntityId; + $uidData .= strlen($attributeValue) . ':' . $attributeValue; + $uidData .= $secretSalt; + + return hash('sha1', $uidData); + } + } + + $attributes = $state['Attributes']; + if (!array_key_exists($attribute, $attributes)) { + Logger::error('Unable to add NameID: Missing ' . var_export($attribute, true) . + ' in the attributes of the user.'); + return null; + } + + return $attributes[$attribute][0]; + } + + + /** + * Helper function for encoding attributes. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array $attributes The attributes of the user. + * + * @return array The encoded attributes. + * + * @throws \SimpleSAML\Error\Exception In case an unsupported encoding is specified by configuration. + */ + private static function encodeAttributes( + Configuration $idpMetadata, + Configuration $spMetadata, + array $attributes + ): array { + $base64Attributes = $spMetadata->getBoolean('base64attributes', null); + if ($base64Attributes === null) { + $base64Attributes = $idpMetadata->getBoolean('base64attributes', false); + } + + if ($base64Attributes) { + $defaultEncoding = 'base64'; + } else { + $defaultEncoding = 'string'; + } + + $srcEncodings = $idpMetadata->getArray('attributeencodings', []); + $dstEncodings = $spMetadata->getArray('attributeencodings', []); + + /* + * Merge the two encoding arrays. Encodings specified in the target metadata + * takes precedence over the source metadata. + */ + $encodings = array_merge($srcEncodings, $dstEncodings); + + $ret = []; + foreach ($attributes as $name => $values) { + $ret[$name] = []; + if (array_key_exists($name, $encodings)) { + $encoding = $encodings[$name]; + } else { + $encoding = $defaultEncoding; + } + + foreach ($values as $value) { + // allow null values + if ($value === null) { + $ret[$name][] = $value; + continue; + } + + $attrval = $value; + if ($value instanceof DOMNodeList) { + /** @psalm-suppress PossiblyNullPropertyFetch */ + $attrval = new AttributeValue($value->item(0)->parentNode); + } + + switch ($encoding) { + case 'string': + $value = (string) $attrval; + break; + case 'base64': + $value = base64_encode((string) $attrval); + break; + case 'raw': + if (is_string($value)) { + $doc = DOMDocumentFactory::fromString('' . $value . ''); + $value = $doc->firstChild->childNodes; + } + assert($value instanceof DOMNodeList || $value instanceof NameID); + break; + default: + throw new Error\Exception('Invalid encoding for attribute ' . + var_export($name, true) . ': ' . var_export($encoding, true)); + } + $ret[$name][] = $value; + } + } + + return $ret; + } + + + /** + * Determine which NameFormat we should use for attributes. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * + * @return string The NameFormat. + */ + private static function getAttributeNameFormat( + Configuration $idpMetadata, + Configuration $spMetadata + ): string { + // try SP metadata first + $attributeNameFormat = $spMetadata->getString('attributes.NameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + $attributeNameFormat = $spMetadata->getString('AttributeNameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + + // look in IdP metadata + $attributeNameFormat = $idpMetadata->getString('attributes.NameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + $attributeNameFormat = $idpMetadata->getString('AttributeNameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + + // default + return Constants::NAMEFORMAT_BASIC; + } + + + /** + * Build an assertion based on information in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array &$state The state array with information about the request. + * + * @return \SAML2\Assertion The assertion. + * + * @throws \SimpleSAML\Error\Exception In case an error occurs when creating a holder-of-key assertion. + */ + private static function buildAssertion( + Configuration $idpMetadata, + Configuration $spMetadata, + array &$state + ): Assertion { + assert(isset($state['Attributes'])); + assert(isset($state['saml:ConsumerURL'])); + + $now = time(); + + $signAssertion = $spMetadata->getBoolean('saml20.sign.assertion', null); + if ($signAssertion === null) { + $signAssertion = $idpMetadata->getBoolean('saml20.sign.assertion', true); + } + + $config = Configuration::getInstance(); + + $a = new Assertion(); + if ($signAssertion) { + \SimpleSAML\Module\saml\Message::addSign($idpMetadata, $spMetadata, $a); + } + + $issuer = new Issuer(); + $issuer->setValue($idpMetadata->getString('entityid')); + $issuer->setFormat(Constants::NAMEID_ENTITY); + $a->setIssuer($issuer); + + $audience = array_merge([$spMetadata->getString('entityid')], $spMetadata->getArray('audience', [])); + $a->setValidAudiences($audience); + + $a->setNotBefore($now - 30); + + $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', null); + if ($assertionLifetime === null) { + $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300); + } + $a->setNotOnOrAfter($now + $assertionLifetime); + + if (isset($state['saml:AuthnContextClassRef'])) { + $a->setAuthnContextClassRef($state['saml:AuthnContextClassRef']); + } elseif (Utils\HTTP::isHTTPS()) { + $a->setAuthnContextClassRef(Constants::AC_PASSWORD_PROTECTED_TRANSPORT); + } else { + $a->setAuthnContextClassRef(Constants::AC_PASSWORD); + } + + $sessionStart = $now; + if (isset($state['AuthnInstant'])) { + $a->setAuthnInstant($state['AuthnInstant']); + $sessionStart = $state['AuthnInstant']; + } + + $sessionLifetime = $config->getInteger('session.duration', 8 * 60 * 60); + $a->setSessionNotOnOrAfter($sessionStart + $sessionLifetime); + + $a->setSessionIndex(Utils\Random::generateID()); + + $sc = new SubjectConfirmation(); + $scd = new SubjectConfirmationData(); + $scd->setNotOnOrAfter($now + $assertionLifetime); + $scd->setRecipient($state['saml:ConsumerURL']); + $scd->setInResponseTo($state['saml:RequestId']); + $sc->setSubjectConfirmationData($scd); + + // ProtcolBinding of SP's overwrites IdP hosted metadata configuration + $hokAssertion = null; + if ($state['saml:Binding'] === Constants::BINDING_HOK_SSO) { + $hokAssertion = true; + } + if ($hokAssertion === null) { + $hokAssertion = $idpMetadata->getBoolean('saml20.hok.assertion', false); + } + + if ($hokAssertion) { + // Holder-of-Key + $sc->setMethod(Constants::CM_HOK); + if (Utils\HTTP::isHTTPS()) { + if (isset($_SERVER['SSL_CLIENT_CERT']) && !empty($_SERVER['SSL_CLIENT_CERT'])) { + // extract certificate data (if this is a certificate) + $clientCert = $_SERVER['SSL_CLIENT_CERT']; + $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; + if (preg_match($pattern, $clientCert, $matches)) { + // we have a client certificate from the browser which we add to the HoK assertion + $x509Certificate = new X509Certificate(); + $x509Certificate->setCertificate(str_replace(["\r", "\n", " "], '', $matches[1])); + + $x509Data = new X509Data(); + $x509Data->addData($x509Certificate); + + $keyInfo = new KeyInfo(); + $keyInfo->addInfo($x509Data); + + $scd->addInfo($keyInfo); + } else { + throw new Error\Exception( + 'Error creating HoK assertion: No valid client certificate provided during ' + . 'TLS handshake with IdP' + ); + } + } else { + throw new Error\Exception( + 'Error creating HoK assertion: No client certificate provided during TLS handshake with IdP' + ); + } + } else { + throw new Error\Exception( + 'Error creating HoK assertion: No HTTPS connection to IdP, but required for Holder-of-Key SSO' + ); + } + } else { + // Bearer + $sc->setMethod(Constants::CM_BEARER); + } + $sc->setSubjectConfirmationData($scd); + $a->setSubjectConfirmation([$sc]); + + // add attributes + if ($spMetadata->getBoolean('simplesaml.attributes', true)) { + $attributeNameFormat = self::getAttributeNameFormat($idpMetadata, $spMetadata); + $a->setAttributeNameFormat($attributeNameFormat); + $attributes = self::encodeAttributes($idpMetadata, $spMetadata, $state['Attributes']); + $a->setAttributes($attributes); + } + + $nameIdFormat = null; + + // generate the NameID for the assertion + if (isset($state['saml:NameIDFormat'])) { + $nameIdFormat = $state['saml:NameIDFormat']; + } + + if ($nameIdFormat === null || !isset($state['saml:NameID'][$nameIdFormat])) { + // either not set in request, or not set to a format we supply. Fall back to old generation method + $nameIdFormat = current($spMetadata->getArrayizeString('NameIDFormat', [])); + if ($nameIdFormat === false) { + $nameIdFormat = current($idpMetadata->getArrayizeString('NameIDFormat', [Constants::NAMEID_TRANSIENT])); + } + } + + if (isset($state['saml:NameID'][$nameIdFormat])) { + $nameId = $state['saml:NameID'][$nameIdFormat]; + $nameId->setFormat($nameIdFormat); + } else { + $spNameQualifier = $spMetadata->getString('SPNameQualifier', null); + if ($spNameQualifier === null) { + $spNameQualifier = $spMetadata->getString('entityid'); + } + + if ($nameIdFormat === Constants::NAMEID_TRANSIENT) { + // generate a random id + $nameIdValue = Utils\Random::generateID(); + } else { + /* this code will end up generating either a fixed assigned id (via nameid.attribute) + or random id if not assigned/configured */ + $nameIdValue = self::generateNameIdValue($idpMetadata, $spMetadata, $state); + if ($nameIdValue === null) { + Logger::warning('Falling back to transient NameID.'); + $nameIdFormat = Constants::NAMEID_TRANSIENT; + $nameIdValue = Utils\Random::generateID(); + } + } + + $nameId = new NameID(); + $nameId->setFormat($nameIdFormat); + $nameId->setValue($nameIdValue); + $nameId->setSPNameQualifier($spNameQualifier); + } + + $state['saml:idp:NameID'] = $nameId; + + $a->setNameId($nameId); + + $encryptNameId = $spMetadata->getBoolean('nameid.encryption', null); + if ($encryptNameId === null) { + $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', false); + } + if ($encryptNameId) { + $a->encryptNameId(\SimpleSAML\Module\saml\Message::getEncryptionKey($spMetadata)); + } + + return $a; + } + + + /** + * Encrypt an assertion. + * + * This function takes in a \SAML2\Assertion and encrypts it if encryption of + * assertions are enabled in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param \SAML2\Assertion $assertion The assertion we are encrypting. + * + * @return \SAML2\Assertion|\SAML2\EncryptedAssertion The assertion. + * + * @throws \SimpleSAML\Error\Exception In case the encryption key type is not supported. + */ + private static function encryptAssertion( + Configuration $idpMetadata, + Configuration $spMetadata, + Assertion $assertion + ) { + $encryptAssertion = $spMetadata->getBoolean('assertion.encryption', null); + if ($encryptAssertion === null) { + $encryptAssertion = $idpMetadata->getBoolean('assertion.encryption', false); + } + if (!$encryptAssertion) { + // we are _not_ encrypting this assertion, and are therefore done + return $assertion; + } + + + $sharedKey = $spMetadata->getString('sharedkey', null); + if ($sharedKey !== null) { + $algo = $spMetadata->getString('sharedkey_algorithm', null); + if ($algo === null) { + $algo = $idpMetadata->getString('sharedkey_algorithm'); + } + + $key = new XMLSecurityKey($algo); + $key->loadKey($sharedKey); + } else { + $keys = $spMetadata->getPublicKeys('encryption', true); + if (!empty($keys)) { + $key = $keys[0]; + switch ($key['type']) { + case 'X509Certificate': + $pemKey = "-----BEGIN CERTIFICATE-----\n" . + chunk_split($key['X509Certificate'], 64) . + "-----END CERTIFICATE-----\n"; + break; + default: + throw new Error\Exception('Unsupported encryption key type: ' . $key['type']); + } + + // extract the public key from the certificate for encryption + $key = new XMLSecurityKey(XMLSecurityKey::RSA_OAEP_MGF1P, ['type' => 'public']); + $key->loadKey($pemKey); + } else { + throw new Error\ConfigurationError( + 'Missing encryption key for entity `' . $spMetadata->getString('entityid') . '`', + $spMetadata->getString('metadata-set') . '.php', + null + ); + } + } + + $ea = new EncryptedAssertion(); + $ea->setAssertion($assertion, $key); + return $ea; + } + + + /** + * Build a logout request based on information in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array $association The SP association. + * @param string|null $relayState An id that should be carried across the logout. + * + * @return \SAML2\LogoutRequest The corresponding SAML2 logout request. + */ + private static function buildLogoutRequest( + Configuration $idpMetadata, + Configuration $spMetadata, + array $association, + string $relayState = null + ): LogoutRequest { + $lr = \SimpleSAML\Module\saml\Message::buildLogoutRequest($idpMetadata, $spMetadata); + $lr->setRelayState($relayState); + $lr->setSessionIndex($association['saml:SessionIndex']); + $lr->setNameId($association['saml:NameID']); + + $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', null); + if ($assertionLifetime === null) { + $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300); + } + $lr->setNotOnOrAfter(time() + $assertionLifetime); + + $encryptNameId = $spMetadata->getBoolean('nameid.encryption', null); + if ($encryptNameId === null) { + $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', false); + } + if ($encryptNameId) { + $lr->encryptNameId(\SimpleSAML\Module\saml\Message::getEncryptionKey($spMetadata)); + } + + return $lr; + } + + + /** + * Build a authentication response based on information in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param string $consumerURL The Destination URL of the response. + * + * @return \SAML2\Response The SAML2 Response corresponding to the given data. + */ + private static function buildResponse( + Configuration $idpMetadata, + Configuration $spMetadata, + string $consumerURL + ): Response { + $signResponse = $spMetadata->getBoolean('saml20.sign.response', null); + if ($signResponse === null) { + $signResponse = $idpMetadata->getBoolean('saml20.sign.response', true); + } + + $r = new Response(); + $issuer = new Issuer(); + $issuer->setValue($idpMetadata->getString('entityid')); + $issuer->setFormat(Constants::NAMEID_ENTITY); + $r->setIssuer($issuer); + $r->setDestination($consumerURL); + + if ($signResponse) { + \SimpleSAML\Module\saml\Message::addSign($idpMetadata, $spMetadata, $r); + } + + return $r; + } +} \ No newline at end of file diff --git a/modules/sildisco/lib/IdPDisco.php b/modules/sildisco/lib/IdPDisco.php new file mode 100644 index 00000000..0ea08a17 --- /dev/null +++ b/modules/sildisco/lib/IdPDisco.php @@ -0,0 +1,214 @@ +session->getData($sessionDataType, $sessionKeyForSP); */ + public static $sessionDataType = 'sildisco:authentication'; + public static $sessionKeyForSP = 'spentityid'; + + + /** + * Log a message. + * + * This is an helper function for logging messages. It will prefix the messages with our discovery service type. + * + * @param string $message The message which should be logged. + */ + protected function log($message) + { + \SimpleSAML\Logger::info('SildiscoIdPDisco.'.$this->instance.': '.$message); + } + + /* Path to the folder with the SP and IdP metadata */ + private function getMetadataPath() { + return __DIR__ . '/../../../metadata/'; + } + + private function getSPEntityIDAndReducedIdpList() + { + + $idpList = $this->getIdPList(); + $idpList = $this->filterList($idpList); + + $spEntityId = $this->session->getData(self::$sessionDataType, self::$sessionKeyForSP); + + $idpList = DiscoUtils::getReducedIdpList( + $idpList, + $this->getMetadataPath(), + $spEntityId + ); + + return array($spEntityId, self::enableBetaEnabled($idpList)); + } + + /** + * Handles a request to this discovery service. + * + * The IdP disco parameters should be set before calling this function. + */ + public function handleRequest() + { + + $this->start(); + list($spEntityId, $idpList) = $this->getSPEntityIDAndReducedIdpList(); + + if (sizeof($idpList) == 1) { + $idp = array_keys($idpList)[0]; + $idp = $this->validateIdP($idp); + if ($idp !== null) { + + $this->log( + 'Choice made [' . $idp . '] (Redirecting the user back. returnIDParam=' . + $this->returnIdParam . ')' + ); + + \SimpleSAML\Utils\HTTP::redirectTrustedURL( + $this->returnURL, + array($this->returnIdParam => $idp) + ); + } + } + + // Get the SP's name + $spEntries = Metadata::getSpMetadataEntries($this->getMetadataPath()); + + $t = new \SimpleSAML\XHTML\Template($this->config, 'selectidp-links.php', 'disco'); + + $spName = null; + + $rawSPName = $spEntries[$spEntityId][self::$spNameMdKey] ?? null; + if ($rawSPName !== null) { + $spName = htmlspecialchars($t->getTranslator()->getPreferredTranslation( + \SimpleSAML\Utils\Arrays::arrayize($rawSPName, 'en') + )) ; + } + + $t->data['idplist'] = $idpList; + $t->data['return'] = $this->returnURL; + $t->data['returnIDParam'] = $this->returnIdParam; + $t->data['entityID'] = $this->spEntityId; + $t->data['spName'] = $spName; + $t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery()); + $t->data['announcement'] = AnnouncementUtils::getSimpleAnnouncement(); + $t->data['helpCenterUrl'] = $this->config->getValue('helpCenterUrl', ''); + + $t->show(); + } + + /** + * @param array $idpList the IDPs with their metadata + * @param bool $isBetaTester optional (default=null) just for unit testing + * @return array $idpList + * + * If the current user has the beta_tester cookie, then for each IDP in + * the idpList that has 'betaEnabled' => true, give it 'enabled' => true + * + */ + public static function enableBetaEnabled($idpList, $isBetaTester=null) { + + if ( $isBetaTester === null) { + $session = \SimpleSAML\Session::getSessionFromRequest(); + $isBetaTester = $session->getData( + self::$sessionType, + self::$betaTesterSessionKey + ); + } + + if ( ! $isBetaTester) { + return $idpList; + } + + foreach ($idpList as $idp => $idpMetadata) { + if ( ! empty($idpMetadata[self::$betaEnabledMdKey])) { + $idpMetadata[self::$enabledMdKey] = true; + $idpList[$idp] = $idpMetadata; + } + } + + return $idpList; + } + + /** + * Validates the given IdP entity id. + * + * Takes a string with the IdP entity id, and returns the entity id if it is valid, or + * null if not. Ensures that the selected IdP is allowed for the current SP + * + * @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null. + * + * @return string|null The entity id if it is valid, null if not. + */ + protected function validateIdP($idp) + { + if ($idp === null) { + return null; + } + if (!$this->config->getBoolean('idpdisco.validate', true)) { + return $idp; + } + + list($spEntityId, $idpList) = $this->getSPEntityIDAndReducedIdpList(); + + /* + * All this complication is for security. + * Without it a user is able to use his authentication through an + * IdP to login to an SP that normally shouldn't accept that IdP. + * + * With a good process, the current SP's entity ID will appear in the + * session and in the request's 'return' entry. + * + * With a hacked process, the SP in the session will not appear in the + * request's 'return' entry. + */ + $returnKey = 'return'; + $requestReturn = array_key_exists($returnKey, $_REQUEST) ? + urldecode(urldecode($_REQUEST[$returnKey])) : ""; + + $spEntityIdParam = 'spentityid='.$spEntityId; + + if (strpos($requestReturn, $spEntityIdParam) === false) { + $message = 'Invalid SP entity id [' . $spEntityId . ']. ' . + 'Could not find in return value. ' . PHP_EOL . $requestReturn; + $this->log($message); + return null; + } + + + if (array_key_exists($idp, $idpList) && $idpList[$idp]['enabled']) { + return $idp; + } + $this->log('Invalid IdP entity id ['.$idp.'] received from discovery page.'); + // the entity id wasn't valid + return null; + } +} diff --git a/modules/sildisco/lib/SSOService.php b/modules/sildisco/lib/SSOService.php new file mode 100644 index 00000000..45d972dd --- /dev/null +++ b/modules/sildisco/lib/SSOService.php @@ -0,0 +1,47 @@ + + * @package SimpleSAMLphp + */ + +require_once('../../_include.php'); + +\SimpleSAML\Logger::info('SAML2.0 - IdP.SSOService: Accessing SAML 2.0 IdP endpoint SSOService'); + +$metadata = \SimpleSAML\Metadata\MetaDataStorageHandler::getMetadataHandler(); + +$config = \SimpleSAML\Configuration::getInstance(); +if (!$config->getBoolean('enable.saml20-idp', false) || !\SimpleSAML\Module::isModuleEnabled('saml')) { + throw new \SimpleSAML\Error\Error('NOACCESS', null, 403); +} + +$idpEntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted'); +$idp = \SimpleSAML\IdP::getById('saml2:' . $idpEntityId); + +$hubModeKey = 'hubmode'; + +try { +// If in hub mode, then use the sildisco entry script + if ($config->getValue($hubModeKey, false)) { + \SimpleSAML\Module\sildisco\IdP\SAML2::receiveAuthnRequest($idp); + } else { + \SimpleSAML\Module\saml\IdP\SAML2::receiveAuthnRequest($idp); + } +} catch (\Exception $e) { + if ($e->getMessage() === "Unable to find the current binding.") { + throw new \SimpleSAML\Error\Error('SSOPARAMS', $e, 400); + } else { + throw $e; // do not ignore other exceptions! + } +} +assert(false); diff --git a/modules/sildisco/tests/AddIdpTest.php b/modules/sildisco/tests/AddIdpTest.php new file mode 100644 index 00000000..40ffcd20 --- /dev/null +++ b/modules/sildisco/tests/AddIdpTest.php @@ -0,0 +1,128 @@ + $idp, + 'saml:sp:NameID' => [ + [ + 'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + 'Value' => 'Tester1_Smith', + 'SPNameQualifier' => 'http://ssp-sp1.local', + ], + ], + 'Attributes' => [], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + } + + /** + * Helper function to run the filter with a given configuration. + * + * @param array $config The filter configuration. + * @param array $request The request state. + * @return array The state array after processing. + */ + private static function processAddIdp2NameId(array $config, array $request) + { + $filter = new \SimpleSAML\Module\sildisco\Auth\Process\AddIdp2NameId($config, NULL); + $filter->process($request); + return $request; + } + + /* + * Test with IdP metadata not having an IDPNamespace entry + * @expectedException \SimpleSAML\Error\Exception + */ + public function testAddIdp2NameId_NoIDPNamespace() + { + $this->setExpectedException('\SimpleSAML\Error\Exception'); + $config = [ 'test' => ['value1', 'value2'], ]; + $request = self::getNameID('idp-bare'); + + self::processAddIdp2NameId($config, $request); + } + + + /* + * Test with IdP metadata not having an IDPNamespace entry + * @expectedException \SimpleSAML\Error\Exception + */ + public function testAddIdp2NameId_EmptyIDPNamespace() + { + $this->setExpectedException('\SimpleSAML\Error\Exception'); + $config = [ 'test' => ['value1', 'value2'], ]; + $request = self::getNameID('idp-empty'); + self::processAddIdp2NameId($config, $request); + } + + /* + * Test with IdP metadata not having an IDPNamespace entry + * @expectedException \SimpleSAML\Error\Exception + */ + public function testAddIdp2NameId_BadIDPNamespace() + { + $this->setExpectedException('\SimpleSAML\Error\Exception'); + $config = [ + 'test' => ['value1', 'value2'], + ]; + $request = self::getNameID('idp-bad'); + self::processAddIdp2NameId($config, $request); + } + + + + /* + * Test with IdP metadata having a good IDPNamespace entry + */ + public function testAddIdp2NameId_GoodString() + { + $config = ['test' => ['value1', 'value2']]; + $state = [ + 'saml:sp:IdP' => 'idp-good', + 'saml:sp:NameID' => 'Tester1_SmithA', + 'Attributes' => [], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $newNameID = $state['saml:sp:NameID']; + $newNameID = 'Tester1_SmithA@idpGood'; + + $expected = $state; + $expected['saml:NameID']['urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'] = $newNameID; + + $results = self::processAddIdp2NameId($config, $state); + $this->assertEquals($expected, $results); + } + /* + * Test with IdP metadata having a good IDPNamespace entry + */ + public function testAddIdp2NameId_GoodArray() + { + $config = ['test' => ['value1', 'value2']]; + $state = [ + 'saml:sp:IdP' => 'idp-good', + 'saml:sp:NameID' => [ + 'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:transient', + 'Value' => 'Tester1_SmithA', + 'SPNameQualifier' => 'http://ssp-sp1.local', + ], + 'Attributes' => [], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $newNameID = $state['saml:sp:NameID']; + $newNameID['Value'] = 'Tester1_SmithA@idpGood'; + + $expected = $state; + $expected['saml:NameID']['urn:oasis:names:tc:SAML:1.1:nameid-format:transient'] = $newNameID; + + $results = self::processAddIdp2NameId($config, $state); + + $this->assertEquals($expected, $results); + } + +} diff --git a/modules/sildisco/tests/TagGroupTest.php b/modules/sildisco/tests/TagGroupTest.php new file mode 100644 index 00000000..f00a601c --- /dev/null +++ b/modules/sildisco/tests/TagGroupTest.php @@ -0,0 +1,106 @@ +process($request); + return $request; + } + + /* + * Test with oid and friendly keys for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_Both() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-bare', + "Attributes" => [ + 'urn:oid:2.5.4.31' => ['ADMINS'], + 'member' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['urn:oid:2.5.4.31'] = ['idp|idp-bare|ADMINS']; + $expected["Attributes"]['member'] = ['idp|idp-bare|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } + + + /* + * Test with friendly key for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_Member() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-bare', + "Attributes" => [ + 'member' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['member'] = ['idp|idp-bare|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } + + /* + * Test with oid key for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_Oid() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-bare', + "Attributes" => [ + 'urn:oid:2.5.4.31' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['urn:oid:2.5.4.31'] = ['idp|idp-bare|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } + + /* + * Test with oid key for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_IdpGood() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-good', + "Attributes" => [ + 'urn:oid:2.5.4.31' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['urn:oid:2.5.4.31'] = ['idp|idpGood|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } +} diff --git a/modules/sildisco/tests/fixtures/metadata/idp-bad-code.php b/modules/sildisco/tests/fixtures/metadata/idp-bad-code.php new file mode 100644 index 00000000..efbce0e7 --- /dev/null +++ b/modules/sildisco/tests/fixtures/metadata/idp-bad-code.php @@ -0,0 +1,12 @@ + [ + 'SingleSignOnService' => 'http://idp-empty/saml2/idp/SSOService.php', + 'IDPNamespace' => '', + ], + 'idp-bad' => [ + 'SingleSignOnService' => 'http://idp-bad/saml2/idp/SSOService.php', + 'IDPNamespace' => 'ba!d!', + ], +]; \ No newline at end of file diff --git a/modules/sildisco/tests/fixtures/metadata/idp-bare.php b/modules/sildisco/tests/fixtures/metadata/idp-bare.php new file mode 100644 index 00000000..c2d28c07 --- /dev/null +++ b/modules/sildisco/tests/fixtures/metadata/idp-bare.php @@ -0,0 +1,7 @@ + [ + 'SingleSignOnService' => 'http://idp-bare/saml2/idp/SSOService.php', + ], +]; diff --git a/modules/sildisco/tests/fixtures/metadata/idp-good.php b/modules/sildisco/tests/fixtures/metadata/idp-good.php new file mode 100644 index 00000000..c06cfa22 --- /dev/null +++ b/modules/sildisco/tests/fixtures/metadata/idp-good.php @@ -0,0 +1,8 @@ + [ + 'SingleSignOnService' => 'http://idp-bare/saml2/idp/SSOService.php', + 'IDPNamespace' => 'idpGood', + ], +]; diff --git a/modules/sildisco/tests/phpunit.xml b/modules/sildisco/tests/phpunit.xml new file mode 100644 index 00000000..807ed707 --- /dev/null +++ b/modules/sildisco/tests/phpunit.xml @@ -0,0 +1,30 @@ + + + + + ../tests/ + + + + + ../tests/ + + ../fixtures/ + + + + + + + + + \ No newline at end of file diff --git a/modules/sildisco/www/betatest.php b/modules/sildisco/www/betatest.php new file mode 100644 index 00000000..b4facf87 --- /dev/null +++ b/modules/sildisco/www/betatest.php @@ -0,0 +1,12 @@ +setData($sessionType, $sessionKey, 1, \SimpleSAML\Session::DATA_TIMEOUT_SESSION_END); + +echo "

Start Beta Testing

"; +echo "

You have been given a cookie to allow you to test beta-enabled IDPs.

"; +echo "

To remove the cookie, just close your browser.

"; diff --git a/modules/sildisco/www/disco.php b/modules/sildisco/www/disco.php new file mode 100644 index 00000000..6c3c08f0 --- /dev/null +++ b/modules/sildisco/www/disco.php @@ -0,0 +1,9 @@ +handleRequest(); diff --git a/modules/sildisco/www/metadata.php b/modules/sildisco/www/metadata.php new file mode 100644 index 00000000..d040bdc3 --- /dev/null +++ b/modules/sildisco/www/metadata.php @@ -0,0 +1,224 @@ +getBoolean('enable.saml20-idp', false)) { + throw new \SimpleSAML\Error\Error('NOACCESS'); +} + +// check if valid local session exists +//if ($config->getBoolean('admin.protectmetadata', false)) { +// Auth::requireAdmin(); +//} + +try { + $idpentityid = isset($_GET['idpentityid']) ? + $_GET['idpentityid'] : + $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted'); + $idpmeta = $metadata->getMetaDataConfig($idpentityid, 'saml20-idp-hosted'); + + $availableCerts = array(); + + $keys = array(); + $certInfo = Crypto::loadPublicKey($idpmeta, false, 'new_'); + if ($certInfo !== null) { + $availableCerts['new_idp.crt'] = $certInfo; + $keys[] = array( + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => true, + 'X509Certificate' => $certInfo['certData'], + ); + $hasNewCert = true; + } else { + $hasNewCert = false; + } + + $certInfo = Crypto::loadPublicKey($idpmeta, true); + $availableCerts['idp.crt'] = $certInfo; + $keys[] = array( + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => ($hasNewCert ? false : true), + 'X509Certificate' => $certInfo['certData'], + ); + + if ($idpmeta->hasValue('https.certificate')) { + $httpsCert = Crypto::loadPublicKey($idpmeta, true, 'https.'); + assert('isset($httpsCert["certData"])'); + $availableCerts['https.crt'] = $httpsCert; + $keys[] = array( + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => false, + 'X509Certificate' => $httpsCert['certData'], + ); + } + + $metaArray = array( + 'metadata-set' => 'saml20-idp-remote', + 'entityid' => $idpentityid, + ); + + $ssob = $metadata->getGenerated('SingleSignOnServiceBinding', 'saml20-idp-hosted'); + $slob = $metadata->getGenerated('SingleLogoutServiceBinding', 'saml20-idp-hosted'); + $ssol = $metadata->getGenerated('SingleSignOnService', 'saml20-idp-hosted'); + $slol = $metadata->getGenerated('SingleLogoutService', 'saml20-idp-hosted'); + + if (is_array($ssob)) { + foreach ($ssob as $binding) { + $metaArray['SingleSignOnService'][] = array( + 'Binding' => $binding, + 'Location' => $ssol, + ); + } + } else { + $metaArray['SingleSignOnService'][] = array( + 'Binding' => $ssob, + 'Location' => $ssol, + ); + } + + if (is_array($slob)) { + foreach ($slob as $binding) { + $metaArray['SingleLogoutService'][] = array( + 'Binding' => $binding, + 'Location' => $slol, + ); + } + } else { + $metaArray['SingleLogoutService'][] = array( + 'Binding' => $slob, + 'Location' => $slol, + ); + } + + if (count($keys) === 1) { + $metaArray['certData'] = $keys[0]['X509Certificate']; + } else { + $metaArray['keys'] = $keys; + } + + if ($idpmeta->getBoolean('saml20.sendartifact', false)) { + // Artifact sending enabled + $metaArray['ArtifactResolutionService'][] = array( + 'index' => 0, + 'Location' => HTTP::getBaseURL().'saml2/idp/ArtifactResolutionService.php', + 'Binding' => Constants::BINDING_SOAP, + ); + } + + if ($idpmeta->getBoolean('saml20.hok.assertion', false)) { + // Prepend HoK SSO Service endpoint. + array_unshift($metaArray['SingleSignOnService'], array( + 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => Constants::BINDING_HOK_SSO, + 'Location' => HTTP::getBaseURL().'saml2/idp/SSOService.php' + )); + } + + $metaArray['NameIDFormat'] = $idpmeta->getString( + 'NameIDFormat', + 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + ); + + if ($idpmeta->hasValue('OrganizationName')) { + $metaArray['OrganizationName'] = $idpmeta->getLocalizedString('OrganizationName'); + $metaArray['OrganizationDisplayName'] = $idpmeta->getLocalizedString( + 'OrganizationDisplayName', + $metaArray['OrganizationName'] + ); + + if (!$idpmeta->hasValue('OrganizationURL')) { + throw new \SimpleSAML\Error\Exception('If OrganizationName is set, OrganizationURL must also be set.'); + } + $metaArray['OrganizationURL'] = $idpmeta->getLocalizedString('OrganizationURL'); + } + + if ($idpmeta->hasValue('scope')) { + $metaArray['scope'] = $idpmeta->getArray('scope'); + } + + if ($idpmeta->hasValue('EntityAttributes')) { + $metaArray['EntityAttributes'] = $idpmeta->getArray('EntityAttributes'); + + // check for entity categories + if (Metadata::isHiddenFromDiscovery($metaArray)) { + $metaArray['hide.from.discovery'] = true; + } + } + + if ($idpmeta->hasValue('UIInfo')) { + $metaArray['UIInfo'] = $idpmeta->getArray('UIInfo'); + } + + if ($idpmeta->hasValue('DiscoHints')) { + $metaArray['DiscoHints'] = $idpmeta->getArray('DiscoHints'); + } + + if ($idpmeta->hasValue('RegistrationInfo')) { + $metaArray['RegistrationInfo'] = $idpmeta->getArray('RegistrationInfo'); + } + + if ($idpmeta->hasValue('validate.authnrequest')) { + $metaArray['sign.authnrequest'] = $idpmeta->getBoolean('validate.authnrequest'); + } + + if ($idpmeta->hasValue('redirect.validate')) { + $metaArray['redirect.sign'] = $idpmeta->getBoolean('redirect.validate'); + } + + if ($idpmeta->hasValue('contacts')) { + $contacts = $idpmeta->getArray('contacts'); + foreach ($contacts as $contact) { + $metaArray['contacts'][] = Metadata::getContact($contact); + } + } + + $technicalContactEmail = $config->getString('technicalcontact_email', false); + if ($technicalContactEmail && $technicalContactEmail !== 'na@example.org') { + $techcontact['emailAddress'] = $technicalContactEmail; + $techcontact['name'] = $config->getString('technicalcontact_name', null); + $techcontact['contactType'] = 'technical'; + $metaArray['contacts'][] = Metadata::getContact($techcontact); + } + + $metaBuilder = new \SimpleSAML\Metadata\SAMLBuilder($idpentityid); + $metaBuilder->addMetadataIdP20($metaArray); + $metaBuilder->addOrganizationInfo($metaArray); + + $metaxml = $metaBuilder->getEntityDescriptorText(); + + $metaflat = '$metadata['.var_export($idpentityid, true).'] = '.var_export($metaArray, true).';'; + + // sign the metadata if enabled + $metaxml = \SimpleSAML\Metadata\Signer::sign($metaxml, $idpmeta->toArray(), 'SAML 2 IdP'); + + if (array_key_exists('format', $_GET) && $_GET['format'] == 'xml') { + header('Content-Type: application/xml'); + + echo $metaxml; + exit(0); + } else { + + header('Content-Type: text/html; charset=utf-8'); + + echo '
' . print_r($metaflat, true) . '
'; + exit(0); + } +} catch (Exception $exception) { + throw new \SimpleSAML\Error\Error('METADATA', $exception); +} diff --git a/modules/sildisco/www/sp/discoresp.php b/modules/sildisco/www/sp/discoresp.php new file mode 100644 index 00000000..449309ff --- /dev/null +++ b/modules/sildisco/www/sp/discoresp.php @@ -0,0 +1,34 @@ +startSSO($_REQUEST['idpentityid'], $state); diff --git a/modules/sildisco/www/sp/saml2-acs.php b/modules/sildisco/www/sp/saml2-acs.php new file mode 100644 index 00000000..d5d29aca --- /dev/null +++ b/modules/sildisco/www/sp/saml2-acs.php @@ -0,0 +1,273 @@ +getMetadata(); +try { + $b = \SAML2\Binding::getCurrentBinding(); +} catch (Exception $e) { + // TODO: look for a specific exception + // This is dirty. Instead of checking the message of the exception, \SAML2\Binding::getCurrentBinding() should throw + // a specific exception when the binding is unknown, and we should capture that here + if ($e->getMessage() === 'Unable to find the current binding.') { + throw new \SimpleSAML\Error\Error('ACSPARAMS', $e, 400); + } else { + // do not ignore other exceptions! + throw $e; + } +} + +if ($b instanceof \SAML2\HTTPArtifact) { + $b->setSPMetadata($spMetadata); +} + +$response = $b->receive(); +if (!($response instanceof \SAML2\Response)) { + throw new \SimpleSAML\Error\BadRequest('Invalid message received to AssertionConsumerService endpoint.'); +} + +/** @psalm-var null|string|\SAML2\XML\saml\Issuer $issuer Remove in SSP 2.0 */ +$issuer = $response->getIssuer(); +if ($issuer === null) { + // no Issuer in the response. Look for an unencrypted assertion with an issuer + foreach ($response->getAssertions() as $a) { + if ($a instanceof \SAML2\Assertion) { + // we found an unencrypted assertion, there should be an issuer here + $issuer = $a->getIssuer(); + break; + } + } + /** @psalm-var string|null $issuer Remove in SSP 2.0 */ + if ($issuer === null) { + // no issuer found in the assertions + throw new Exception('Missing in message delivered to AssertionConsumerService.'); + } +} + +if ($issuer instanceof \SAML2\XML\saml\Issuer) { + /** @psalm-var string|null $issuer */ + $issuer = $issuer->getValue(); + if ($issuer === null) { + // no issuer found in the assertions + throw new Exception('Missing in message delivered to AssertionConsumerService.'); + } +} + +$session = \SimpleSAML\Session::getSessionFromRequest(); +$prevAuth = $session->getAuthData($sourceId, 'saml:sp:prevAuth'); +/** @psalm-var string $issuer */ +if ($prevAuth !== null && $prevAuth['id'] === $response->getId() && $prevAuth['issuer'] === $issuer) { + /* OK, it looks like this message has the same issuer + * and ID as the SP session we already have active. We + * therefore assume that the user has somehow triggered + * a resend of the message. + * In that case we may as well just redo the previous redirect + * instead of displaying a confusing error message. + */ + SimpleSAML\Logger::info( + 'Duplicate SAML 2 response detected - ignoring the response and redirecting the user to the correct page.' + ); + if (isset($prevAuth['redirect'])) { + \SimpleSAML\Utils\HTTP::redirectTrustedURL($prevAuth['redirect']); + } + + SimpleSAML\Logger::info('No RelayState or ReturnURL available, cannot redirect.'); + throw new \SimpleSAML\Error\Exception('Duplicate assertion received.'); +} + +$idpMetadata = null; +$state = null; +$stateId = $response->getInResponseTo(); + +if (!empty($stateId)) { + // this should be a response to a request we sent earlier + try { + $state = \SimpleSAML\Auth\State::loadState($stateId, 'saml:sp:sso'); + } catch (Exception $e) { + // something went wrong, + SimpleSAML\Logger::warning('Could not load state specified by InResponseTo: ' . $e->getMessage() . + ' Processing response as unsolicited.'); + } +} + +if ($state) { + // check that the authentication source is correct + assert(array_key_exists('saml:sp:AuthId', $state)); + if ($state['saml:sp:AuthId'] !== $sourceId) { + throw new \SimpleSAML\Error\Exception( + 'The authentication source id in the URL does not match the authentication source which sent the request.' + ); + } + + // check that the issuer is the one we are expecting + assert(array_key_exists('ExpectedIssuer', $state)); + if ($state['ExpectedIssuer'] !== $issuer) { + $idpMetadata = $source->getIdPMetadata($issuer); + $idplist = $idpMetadata->getArrayize('IDPList', []); + if (!in_array($state['ExpectedIssuer'], $idplist, true)) { + SimpleSAML\Logger::warning( + 'The issuer of the response not match to the identity provider we sent the request to.' + ); + } + } +} else { + // this is an unsolicited response + $relaystate = $spMetadata->getString('RelayState', $response->getRelayState()); + $state = [ + 'saml:sp:isUnsolicited' => true, + 'saml:sp:AuthId' => $sourceId, + 'saml:sp:RelayState' => $relaystate === null ? null : \SimpleSAML\Utils\HTTP::checkURLAllowed($relaystate), + ]; +} + +SimpleSAML\Logger::debug('Received SAML2 Response from ' . var_export($issuer, true) . '.'); + +if (is_null($idpMetadata)) { + $idpMetadata = $source->getIdPmetadata($issuer); +} + +try { + $assertions = \SimpleSAML\Module\saml\Message::processResponse($spMetadata, $idpMetadata, $response); +} catch (\SimpleSAML\Module\saml\Error $e) { + // the status of the response wasn't "success" + $e = $e->toException(); + \SimpleSAML\Auth\State::throwException($state, $e); +} + +$authenticatingAuthority = null; +$nameId = null; +$sessionIndex = null; +$expire = null; +$attributes = []; +$foundAuthnStatement = false; + +foreach ($assertions as $assertion) { + // check for duplicate assertion (replay attack) + $store = \SimpleSAML\Store::getInstance(); + if ($store !== false) { + $aID = $assertion->getId(); + if ($store->get('saml.AssertionReceived', $aID) !== null) { + $e = new \SimpleSAML\Error\Exception('Received duplicate assertion.'); + \SimpleSAML\Auth\State::throwException($state, $e); + } + + $notOnOrAfter = $assertion->getNotOnOrAfter(); + if ($notOnOrAfter === null) { + $notOnOrAfter = time() + 24 * 60 * 60; + } else { + $notOnOrAfter += 60; // we allow 60 seconds clock skew, so add it here also + } + + $store->set('saml.AssertionReceived', $aID, true, $notOnOrAfter); + } + + if ($authenticatingAuthority === null) { + $authenticatingAuthority = $assertion->getAuthenticatingAuthority(); + } + if ($nameId === null) { + $nameId = $assertion->getNameId(); + } + if ($sessionIndex === null) { + $sessionIndex = $assertion->getSessionIndex(); + } + if ($expire === null) { + $expire = $assertion->getSessionNotOnOrAfter(); + } + + $attributes = array_merge($attributes, $assertion->getAttributes()); + + if ($assertion->getAuthnInstant() !== null) { + // assertion contains AuthnStatement, since AuthnInstant is a required attribute + $foundAuthnStatement = true; + } +} +$assertion = end($assertions); + +if (!$foundAuthnStatement) { + $e = new \SimpleSAML\Error\Exception('No AuthnStatement found in assertion(s).'); + \SimpleSAML\Auth\State::throwException($state, $e); +} + +if ($expire !== null) { + $logoutExpire = $expire; +} else { + // just expire the logout association 24 hours into the future + $logoutExpire = time() + 24 * 60 * 60; +} + +if (!empty($nameId)) { + // register this session in the logout store + \SimpleSAML\Module\saml\SP\LogoutStore::addSession($sourceId, $nameId, $sessionIndex, $logoutExpire); + + // we need to save the NameID and SessionIndex for logout + $logoutState = [ + 'saml:logout:Type' => 'saml2', + 'saml:logout:IdP' => $issuer, + 'saml:logout:NameID' => $nameId, + 'saml:logout:SessionIndex' => $sessionIndex, + ]; + + $state['saml:sp:NameID'] = $nameId; // no need to mark it as persistent, it already is +} else { + /* + * No NameID provided, we can't logout from this IdP! + * + * Even though interoperability profiles "require" a NameID, the SAML 2.0 standard does not require it to be present + * in assertions. That way, we could have a Subject with only a SubjectConfirmation, or even no Subject element at + * all. + * + * In case we receive a SAML assertion with no NameID, we can be graceful and continue, but we won't be able to + * perform a Single Logout since the SAML logout profile mandates the use of a NameID to identify the individual we + * want to be logged out. In order to minimize the impact of this, we keep logout state information (without saving + * it to the store), marking the IdP as SAML 1.0, which does not implement logout. Then we can safely log the user + * out from the local session, skipping Single Logout upstream to the IdP. + */ + $logoutState = [ + 'saml:logout:Type' => 'saml1', + ]; +} + +$state['LogoutState'] = $logoutState; +$state['saml:AuthenticatingAuthority'] = $authenticatingAuthority; +$state['saml:AuthenticatingAuthority'][] = $issuer; +$state['PersistentAuthData'][] = 'saml:AuthenticatingAuthority'; +$state['saml:AuthnInstant'] = $assertion->getAuthnInstant(); +$state['PersistentAuthData'][] = 'saml:AuthnInstant'; +$state['saml:sp:SessionIndex'] = $sessionIndex; +$state['PersistentAuthData'][] = 'saml:sp:SessionIndex'; +$state['saml:sp:AuthnContext'] = $assertion->getAuthnContextClassRef(); +$state['PersistentAuthData'][] = 'saml:sp:AuthnContext'; + +if ($expire !== null) { + $state['Expire'] = $expire; +} + +// note some information about the authentication, in case we receive the same response again +$state['saml:sp:prevAuth'] = [ + 'id' => $response->getId(), + 'issuer' => $issuer, + 'inResponseTo' => $response->getInResponseTo(), +]; +if (isset($state['\SimpleSAML\Auth\Source.ReturnURL'])) { + $state['saml:sp:prevAuth']['redirect'] = $state['\SimpleSAML\Auth\Source.ReturnURL']; +} elseif (isset($state['saml:sp:RelayState'])) { + $state['saml:sp:prevAuth']['redirect'] = $state['saml:sp:RelayState']; +} +$state['PersistentAuthData'][] = 'saml:sp:prevAuth'; + +$source->handleResponse($state, $issuer, $attributes); +assert(false); \ No newline at end of file diff --git a/modules/sildisco/www/sp/saml2-logout.php b/modules/sildisco/www/sp/saml2-logout.php new file mode 100644 index 00000000..05d9c14b --- /dev/null +++ b/modules/sildisco/www/sp/saml2-logout.php @@ -0,0 +1,155 @@ +getMessage() === 'Unable to find the current binding.') { + throw new \SimpleSAML\Error\Error('SLOSERVICEPARAMS', $e, 400); + } else { + throw $e; // do not ignore other exceptions! + } +} +$message = $binding->receive(); + +$issuer = $message->getIssuer(); +if ($issuer instanceof \SAML2\XML\saml\Issuer) { + $idpEntityId = $issuer->getValue(); +} else { + $idpEntityId = $issuer; +} + +if ($idpEntityId === null) { + // Without an issuer we have no way to respond to the message. + throw new \SimpleSAML\Error\BadRequest('Received message on logout endpoint without issuer.'); +} + +/** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */ +$spEntityId = $source->getEntityId(); + +$metadata = \SimpleSAML\Metadata\MetaDataStorageHandler::getMetadataHandler(); +$idpMetadata = $source->getIdPMetadata($idpEntityId); +$spMetadata = $source->getMetadata(); + +\SimpleSAML\Module\saml\Message::validateMessage($idpMetadata, $spMetadata, $message); + +$destination = $message->getDestination(); +if ($destination !== null && $destination !== \SimpleSAML\Utils\HTTP::getSelfURLNoQuery()) { + throw new \SimpleSAML\Error\Exception('Destination in logout message is wrong.'); +} + +if ($message instanceof \SAML2\LogoutResponse) { + $relayState = $message->getRelayState(); + if ($relayState === null) { + // Somehow, our RelayState has been lost. + throw new \SimpleSAML\Error\BadRequest('Missing RelayState in logout response.'); + } + + if (!$message->isSuccess()) { + \SimpleSAML\Logger::warning( + 'Unsuccessful logout. Status was: ' . \SimpleSAML\Module\saml\Message::getResponseError($message) + ); + } + + $state = \SimpleSAML\Auth\State::loadState($relayState, 'saml:slosent'); + $state['saml:sp:LogoutStatus'] = $message->getStatus(); + \SimpleSAML\Auth\Source::completeLogout($state); +} elseif ($message instanceof \SAML2\LogoutRequest) { + \SimpleSAML\Logger::debug('module/sildisco/sp/logout: Request from ' . $idpEntityId); // GTIS + \SimpleSAML\Logger::stats('saml20-idp-SLO idpinit ' . $spEntityId . ' ' . $idpEntityId); + + if ($message->isNameIdEncrypted()) { + try { + $keys = \SimpleSAML\Module\saml\Message::getDecryptionKeys($idpMetadata, $spMetadata); + } catch (\Exception $e) { + throw new \SimpleSAML\Error\Exception('Error decrypting NameID: ' . $e->getMessage()); + } + + $blacklist = \SimpleSAML\Module\saml\Message::getBlacklistedAlgorithms($idpMetadata, $spMetadata); + + $lastException = null; + foreach ($keys as $i => $key) { + try { + $message->decryptNameId($key, $blacklist); + \SimpleSAML\Logger::debug('Decryption with key #' . $i . ' succeeded.'); + $lastException = null; + break; + } catch (\Exception $e) { + \SimpleSAML\Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage()); + $lastException = $e; + } + } + if ($lastException !== null) { + throw $lastException; + } + } + + $nameId = $message->getNameId(); + $sessionIndexes = $message->getSessionIndexes(); + + /** @psalm-suppress PossiblyNullArgument This will be fixed in saml2 5.0 */ + $numLoggedOut = \SimpleSAML\Module\saml\SP\LogoutStore::logoutSessions($sourceId, $nameId, $sessionIndexes); + if ($numLoggedOut === false) { + // This type of logout was unsupported. Use the old method + $source->handleLogout($idpEntityId); + $numLoggedOut = count($sessionIndexes); + } + + // Create and send response + $lr = \SimpleSAML\Module\saml\Message::buildLogoutResponse($spMetadata, $idpMetadata); + $lr->setRelayState($message->getRelayState()); + $lr->setInResponseTo($message->getId()); + + if ($numLoggedOut < count($sessionIndexes)) { + \SimpleSAML\Logger::warning('Logged out of ' . $numLoggedOut . ' of ' . count($sessionIndexes) . ' sessions.'); + } + + /** @var array $dst */ + $dst = $idpMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + \SAML2\Constants::BINDING_HTTP_REDIRECT, + \SAML2\Constants::BINDING_HTTP_POST + ] + ); + + if (!($binding instanceof \SAML2\SOAP)) { + $binding = \SAML2\Binding::getBinding($dst['Binding']); + if (isset($dst['ResponseLocation'])) { + $dst = $dst['ResponseLocation']; + } else { + $dst = $dst['Location']; + } + $binding->setDestination($dst); + } + $lr->setDestination($dst); + + $binding->send($lr); +} else { + throw new \SimpleSAML\Error\BadRequest('Unknown message received on logout endpoint: ' . get_class($message)); +} diff --git a/tests/IdpDiscoTest.php b/tests/IdpDiscoTest.php new file mode 100644 index 00000000..df5e171a --- /dev/null +++ b/tests/IdpDiscoTest.php @@ -0,0 +1,52 @@ +assertEquals($expected, $results); + } + + public function testEnableBetaEnabledNoChange() + { + $isBetaEnabled = 1; + $enabledKey = IdPDisco::$enabledMdKey; + $idpList = [ + 'idp1' => [$enabledKey => false], + 'idp2' => [$enabledKey => true], + ]; + $expected = $idpList; + + $results = IdPDisco::enableBetaEnabled($idpList, $isBetaEnabled); + $this->assertEquals($expected, $results); + } + + public function testEnableBetaEnabledChange() + { + $isBetaEnabled = 1; + $enabledKey = IdPDisco::$enabledMdKey; + $betaEnabledKey = IdPDisco::$betaEnabledMdKey; + $idpList = [ + 'idp1' => [$enabledKey => false], + 'idp2' => [$enabledKey => true, $betaEnabledKey => true], + 'idp3' => [$enabledKey => false, $betaEnabledKey => true], + 'idp4' => [$enabledKey => false, $betaEnabledKey => false], + ]; + $expected = $idpList; + $expected['idp3'][$enabledKey] = true; + + $results = IdPDisco::enableBetaEnabled($idpList, $isBetaEnabled); + $this->assertEquals($expected, $results); + } + +}