From 44507da91242e2247d168de93aa38ea6b0608315 Mon Sep 17 00:00:00 2001 From: Tim van Dijen <tvdijen@gmail.com> Date: Wed, 4 May 2022 15:22:25 +0200 Subject: [PATCH] Add controllers for the saml-module (#1623) Add some controllers + tests --- composer.lock | 319 +++++---- docs/simplesamlphp-artifact-idp.md | 4 +- docs/simplesamlphp-hok-idp.md | 4 +- docs/simplesamlphp-idp.md | 4 +- lib/SimpleSAML/Configuration.php | 8 +- lib/SimpleSAML/Database.php | 10 +- .../Logger/SyslogLoggingHandler.php | 5 +- lib/SimpleSAML/XHTML/IdPDisco.php | 22 +- metadata-templates/saml20-sp-remote.php | 4 +- modules/admin/lib/Controller/Federation.php | 5 +- modules/core/docs/authproc_attributelimit.md | 8 +- .../core/lib/Auth/Process/AttributeAlter.php | 2 +- modules/core/lib/Controller/Logout.php | 3 +- modules/saml/lib/Auth/Source/SP.php | 19 +- modules/saml/lib/Controller/Disco.php | 49 ++ modules/saml/lib/Controller/Proxy.php | 122 ++++ .../saml/lib/Controller/ServiceProvider.php | 613 ++++++++++++++++++ modules/saml/lib/IdP/SAML2.php | 5 +- modules/saml/routing/routes/routes.yaml | 30 + modules/saml/www/disco.php | 8 - modules/saml/www/proxy/invalid_session.php | 56 -- modules/saml/www/sp/discoresp.php | 33 - modules/saml/www/sp/metadata.php | 51 -- modules/saml/www/sp/saml2-acs.php | 280 -------- modules/saml/www/sp/saml2-logout.php | 160 ----- .../www/sp/wrong_authncontextclassref.php | 8 - phpcs.xml | 7 +- psalm.xml | 1 - tests/modules/saml/lib/Auth/Source/SPTest.php | 7 +- .../modules/saml/lib/Controller/DiscoTest.php | 67 ++ .../modules/saml/lib/Controller/ProxyTest.php | 165 +++++ .../lib/Controller/ServiceProviderTest.php | 438 +++++++++++++ 32 files changed, 1721 insertions(+), 796 deletions(-) create mode 100644 modules/saml/lib/Controller/Disco.php create mode 100644 modules/saml/lib/Controller/Proxy.php create mode 100644 modules/saml/lib/Controller/ServiceProvider.php create mode 100644 modules/saml/routing/routes/routes.yaml delete mode 100644 modules/saml/www/disco.php delete mode 100644 modules/saml/www/proxy/invalid_session.php delete mode 100644 modules/saml/www/sp/discoresp.php delete mode 100644 modules/saml/www/sp/metadata.php delete mode 100644 modules/saml/www/sp/saml2-acs.php delete mode 100644 modules/saml/www/sp/saml2-logout.php delete mode 100644 modules/saml/www/sp/wrong_authncontextclassref.php create mode 100644 tests/modules/saml/lib/Controller/DiscoTest.php create mode 100644 tests/modules/saml/lib/Controller/ProxyTest.php create mode 100644 tests/modules/saml/lib/Controller/ServiceProviderTest.php diff --git a/composer.lock b/composer.lock index 437d7dec1..d19f13809 100644 --- a/composer.lock +++ b/composer.lock @@ -601,16 +601,16 @@ }, { "name": "simplesamlphp/saml2", - "version": "v4.5.1", + "version": "v4.6.0", "source": { "type": "git", "url": "https://github.com/simplesamlphp/saml2.git", - "reference": "88586eb071476a74cd92d50cb789ad45c0b62f1e" + "reference": "46d93daa9a96a1d896fe35cb75897a91e1795442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/saml2/zipball/88586eb071476a74cd92d50cb789ad45c0b62f1e", - "reference": "88586eb071476a74cd92d50cb789ad45c0b62f1e", + "url": "https://api.github.com/repos/simplesamlphp/saml2/zipball/46d93daa9a96a1d896fe35cb75897a91e1795442", + "reference": "46d93daa9a96a1d896fe35cb75897a91e1795442", "shasum": "" }, "require": { @@ -653,22 +653,22 @@ "description": "SAML2 PHP library from SimpleSAMLphp", "support": { "issues": "https://github.com/simplesamlphp/saml2/issues", - "source": "https://github.com/simplesamlphp/saml2/tree/v4.5.1" + "source": "https://github.com/simplesamlphp/saml2/tree/v4.6.0" }, - "time": "2022-03-22T12:02:23+00:00" + "time": "2022-04-08T13:47:36+00:00" }, { "name": "symfony/cache", - "version": "v5.4.6", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "c0718d0e01ac14251a45cc9c8b93716ec41ae64b" + "reference": "ba06841ed293fcaf79a592f59fdaba471f7c756c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/c0718d0e01ac14251a45cc9c8b93716ec41ae64b", - "reference": "c0718d0e01ac14251a45cc9c8b93716ec41ae64b", + "url": "https://api.github.com/repos/symfony/cache/zipball/ba06841ed293fcaf79a592f59fdaba471f7c756c", + "reference": "ba06841ed293fcaf79a592f59fdaba471f7c756c", "shasum": "" }, "require": { @@ -736,7 +736,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v5.4.6" + "source": "https://github.com/symfony/cache/tree/v5.4.7" }, "funding": [ { @@ -752,20 +752,20 @@ "type": "tidelift" } ], - "time": "2022-03-02T12:56:28+00:00" + "time": "2022-03-22T15:31:03+00:00" }, { "name": "symfony/cache-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "ac2e168102a2e06a2624f0379bde94cd5854ced2" + "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/ac2e168102a2e06a2624f0379bde94cd5854ced2", - "reference": "ac2e168102a2e06a2624f0379bde94cd5854ced2", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", + "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", "shasum": "" }, "require": { @@ -815,7 +815,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/cache-contracts/tree/v2.5.1" }, "funding": [ { @@ -831,20 +831,20 @@ "type": "tidelift" } ], - "time": "2021-08-17T14:20:01+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/config", - "version": "v5.4.3", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "d65e1bd990c740e31feb07d2b0927b8d4df9956f" + "reference": "05624c386afa1b4ccc1357463d830fade8d9d404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/d65e1bd990c740e31feb07d2b0927b8d4df9956f", - "reference": "d65e1bd990c740e31feb07d2b0927b8d4df9956f", + "url": "https://api.github.com/repos/symfony/config/zipball/05624c386afa1b4ccc1357463d830fade8d9d404", + "reference": "05624c386afa1b4ccc1357463d830fade8d9d404", "shasum": "" }, "require": { @@ -894,7 +894,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v5.4.3" + "source": "https://github.com/symfony/config/tree/v5.4.7" }, "funding": [ { @@ -910,20 +910,20 @@ "type": "tidelift" } ], - "time": "2022-01-03T09:50:52+00:00" + "time": "2022-03-21T13:42:03+00:00" }, { "name": "symfony/console", - "version": "v5.4.5", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d8111acc99876953f52fe16d4c50eb60940d49ad" + "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d8111acc99876953f52fe16d4c50eb60940d49ad", - "reference": "d8111acc99876953f52fe16d4c50eb60940d49ad", + "url": "https://api.github.com/repos/symfony/console/zipball/900275254f0a1a2afff1ab0e11abd5587a10e1d6", + "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6", "shasum": "" }, "require": { @@ -993,7 +993,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.5" + "source": "https://github.com/symfony/console/tree/v5.4.7" }, "funding": [ { @@ -1009,20 +1009,20 @@ "type": "tidelift" } ], - "time": "2022-02-24T12:45:35+00:00" + "time": "2022-03-31T17:09:19+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.4.6", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "0828fa3e6e436243dbb3dc85abe6b698b3876b89" + "reference": "35588b2afb08ea3a142d62fefdcad4cb09be06ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0828fa3e6e436243dbb3dc85abe6b698b3876b89", - "reference": "0828fa3e6e436243dbb3dc85abe6b698b3876b89", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/35588b2afb08ea3a142d62fefdcad4cb09be06ed", + "reference": "35588b2afb08ea3a142d62fefdcad4cb09be06ed", "shasum": "" }, "require": { @@ -1082,7 +1082,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.6" + "source": "https://github.com/symfony/dependency-injection/tree/v5.4.7" }, "funding": [ { @@ -1098,20 +1098,20 @@ "type": "tidelift" } ], - "time": "2022-03-02T12:42:23+00:00" + "time": "2022-03-08T15:43:06+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -1149,7 +1149,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" }, "funding": [ { @@ -1165,20 +1165,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/error-handler", - "version": "v5.4.3", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "c4ffc2cd919950d13c8c9ce32a70c70214c3ffc5" + "reference": "060bc01856a1846e3e4385261bc9ed11a1dd7b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/c4ffc2cd919950d13c8c9ce32a70c70214c3ffc5", - "reference": "c4ffc2cd919950d13c8c9ce32a70c70214c3ffc5", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/060bc01856a1846e3e4385261bc9ed11a1dd7b6a", + "reference": "060bc01856a1846e3e4385261bc9ed11a1dd7b6a", "shasum": "" }, "require": { @@ -1220,7 +1220,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.4.3" + "source": "https://github.com/symfony/error-handler/tree/v5.4.7" }, "funding": [ { @@ -1236,7 +1236,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2022-03-18T16:21:29+00:00" }, { "name": "symfony/event-dispatcher", @@ -1325,16 +1325,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a" + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", "shasum": "" }, "require": { @@ -1384,7 +1384,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.1" }, "funding": [ { @@ -1400,20 +1400,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.6", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d53a45039974952af7f7ebc461ccdd4295e29440" + "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d53a45039974952af7f7ebc461ccdd4295e29440", - "reference": "d53a45039974952af7f7ebc461ccdd4295e29440", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3a4442138d80c9f7b600fb297534ac718b61d37f", + "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f", "shasum": "" }, "require": { @@ -1448,7 +1448,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.6" + "source": "https://github.com/symfony/filesystem/tree/v5.4.7" }, "funding": [ { @@ -1464,7 +1464,7 @@ "type": "tidelift" } ], - "time": "2022-03-02T12:42:23+00:00" + "time": "2022-04-01T12:33:59+00:00" }, { "name": "symfony/finder", @@ -1531,16 +1531,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v5.4.6", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "76ea755f30924924ea37a28e098df61679efcb63" + "reference": "7520f553c7a7721652c1b7ac95c09dae62a1676e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/76ea755f30924924ea37a28e098df61679efcb63", - "reference": "76ea755f30924924ea37a28e098df61679efcb63", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/7520f553c7a7721652c1b7ac95c09dae62a1676e", + "reference": "7520f553c7a7721652c1b7ac95c09dae62a1676e", "shasum": "" }, "require": { @@ -1610,7 +1610,6 @@ "symfony/messenger": "^5.4|^6.0", "symfony/mime": "^4.4|^5.0|^6.0", "symfony/notifier": "^5.4|^6.0", - "symfony/phpunit-bridge": "^5.3|^6.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/process": "^4.4|^5.0|^6.0", "symfony/property-info": "^4.4|^5.0|^6.0", @@ -1663,7 +1662,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v5.4.6" + "source": "https://github.com/symfony/framework-bundle/tree/v5.4.7" }, "funding": [ { @@ -1679,7 +1678,7 @@ "type": "tidelift" } ], - "time": "2022-03-04T14:13:35+00:00" + "time": "2022-04-01T06:09:41+00:00" }, { "name": "symfony/http-foundation", @@ -1756,16 +1755,16 @@ }, { "name": "symfony/http-kernel", - "version": "v5.4.6", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "d41f29ae9af1b5f40c7ebcddf09082953229411d" + "reference": "509243b9b3656db966284c45dffce9316c1ecc5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/d41f29ae9af1b5f40c7ebcddf09082953229411d", - "reference": "d41f29ae9af1b5f40c7ebcddf09082953229411d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/509243b9b3656db966284c45dffce9316c1ecc5c", + "reference": "509243b9b3656db966284c45dffce9316c1ecc5c", "shasum": "" }, "require": { @@ -1848,7 +1847,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.6" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.7" }, "funding": [ { @@ -1864,7 +1863,7 @@ "type": "tidelift" } ], - "time": "2022-03-05T21:14:51+00:00" + "time": "2022-04-02T06:04:20+00:00" }, { "name": "symfony/intl", @@ -2617,22 +2616,22 @@ }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -2680,7 +2679,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" }, "funding": [ { @@ -2696,7 +2695,7 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2022-03-13T20:07:29+00:00" }, { "name": "symfony/string", @@ -2786,16 +2785,16 @@ }, { "name": "symfony/translation-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "d28150f0f44ce854e942b671fc2620a98aae1b1e" + "reference": "1211df0afa701e45a04253110e959d4af4ef0f07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/d28150f0f44ce854e942b671fc2620a98aae1b1e", - "reference": "d28150f0f44ce854e942b671fc2620a98aae1b1e", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/1211df0afa701e45a04253110e959d4af4ef0f07", + "reference": "1211df0afa701e45a04253110e959d4af4ef0f07", "shasum": "" }, "require": { @@ -2844,7 +2843,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/translation-contracts/tree/v2.5.1" }, "funding": [ { @@ -2860,20 +2859,20 @@ "type": "tidelift" } ], - "time": "2021-08-17T14:20:01+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/twig-bridge", - "version": "v5.4.5", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "648c8694a9470ae4aaf64cbce1b640f5941fd7c9" + "reference": "b43e9bdb57a39ffffb4c44a7ef0a47d338e9f1da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/648c8694a9470ae4aaf64cbce1b640f5941fd7c9", - "reference": "648c8694a9470ae4aaf64cbce1b640f5941fd7c9", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/b43e9bdb57a39ffffb4c44a7ef0a47d338e9f1da", + "reference": "b43e9bdb57a39ffffb4c44a7ef0a47d338e9f1da", "shasum": "" }, "require": { @@ -2965,7 +2964,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v5.4.5" + "source": "https://github.com/symfony/twig-bridge/tree/v5.4.7" }, "funding": [ { @@ -2981,7 +2980,7 @@ "type": "tidelift" } ], - "time": "2022-02-09T08:59:58+00:00" + "time": "2022-04-01T06:09:41+00:00" }, { "name": "symfony/var-dumper", @@ -3074,16 +3073,16 @@ }, { "name": "symfony/var-exporter", - "version": "v5.4.6", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "49e2355fe6f59ea30c18ebb68edf13b7e20582e5" + "reference": "7eacaa588c9b27f2738575adb4a8457a80d9c807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/49e2355fe6f59ea30c18ebb68edf13b7e20582e5", - "reference": "49e2355fe6f59ea30c18ebb68edf13b7e20582e5", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7eacaa588c9b27f2738575adb4a8457a80d9c807", + "reference": "7eacaa588c9b27f2738575adb4a8457a80d9c807", "shasum": "" }, "require": { @@ -3127,7 +3126,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v5.4.6" + "source": "https://github.com/symfony/var-exporter/tree/v5.4.7" }, "funding": [ { @@ -3143,7 +3142,7 @@ "type": "tidelift" } ], - "time": "2022-03-02T12:42:23+00:00" + "time": "2022-03-31T17:09:19+00:00" }, { "name": "symfony/yaml", @@ -3291,16 +3290,16 @@ }, { "name": "twig/twig", - "version": "v3.3.8", + "version": "v3.3.10", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "972d8604a92b7054828b539f2febb0211dd5945c" + "reference": "8442df056c51b706793adf80a9fd363406dd3674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/972d8604a92b7054828b539f2febb0211dd5945c", - "reference": "972d8604a92b7054828b539f2febb0211dd5945c", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/8442df056c51b706793adf80a9fd363406dd3674", + "reference": "8442df056c51b706793adf80a9fd363406dd3674", "shasum": "" }, "require": { @@ -3351,7 +3350,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.3.8" + "source": "https://github.com/twigphp/Twig/tree/v3.3.10" }, "funding": [ { @@ -3363,7 +3362,7 @@ "type": "tidelift" } ], - "time": "2022-02-04T06:59:48+00:00" + "time": "2022-04-06T06:47:41+00:00" }, { "name": "webmozart/assert", @@ -3737,16 +3736,16 @@ }, { "name": "composer/semver", - "version": "3.3.1", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71" + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/5d8e574bb0e69188786b8ef77d43341222a41a71", - "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", "shasum": "" }, "require": { @@ -3798,7 +3797,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.1" + "source": "https://github.com/composer/semver/tree/3.3.2" }, "funding": [ { @@ -3814,7 +3813,7 @@ "type": "tidelift" } ], - "time": "2022-03-16T11:22:07+00:00" + "time": "2022-04-01T19:23:25+00:00" }, { "name": "composer/xdebug-handler", @@ -4036,16 +4035,16 @@ }, { "name": "felixfbecker/language-server-protocol", - "version": "1.5.1", + "version": "v1.5.2", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730" + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730", - "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", "shasum": "" }, "require": { @@ -4086,9 +4085,9 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" }, - "time": "2021-02-22T14:02:09+00:00" + "time": "2022-03-02T22:36:06+00:00" }, { "name": "mikey179/vfsstream", @@ -4583,16 +4582,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.0", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" + "reference": "77a32518733312af16a44300404e945338981de3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", + "reference": "77a32518733312af16a44300404e945338981de3", "shasum": "" }, "require": { @@ -4627,9 +4626,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" }, - "time": "2022-01-04T19:58:01+00:00" + "time": "2022-03-15T21:29:03+00:00" }, { "name": "phpspec/prophecy", @@ -5018,16 +5017,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.19", + "version": "9.5.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "35ea4b7f3acabb26f4bb640f8c30866c401da807" + "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/35ea4b7f3acabb26f4bb640f8c30866c401da807", - "reference": "35ea4b7f3acabb26f4bb640f8c30866c401da807", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", + "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", "shasum": "" }, "require": { @@ -5105,7 +5104,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.19" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" }, "funding": [ { @@ -5117,7 +5116,7 @@ "type": "github" } ], - "time": "2022-03-15T09:57:31+00:00" + "time": "2022-04-01T12:37:26+00:00" }, { "name": "sebastian/cli-parser", @@ -5485,16 +5484,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", "shasum": "" }, "require": { @@ -5536,7 +5535,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" }, "funding": [ { @@ -5544,7 +5543,7 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2022-04-03T09:37:03+00:00" }, { "name": "sebastian/exporter", @@ -6123,16 +6122,16 @@ }, { "name": "simplesamlphp/simplesamlphp-module-adfs", - "version": "v2.0.0-rc8", + "version": "v2.0.0-rc9", "source": { "type": "git", "url": "https://github.com/simplesamlphp/simplesamlphp-module-adfs.git", - "reference": "ccb86deb3dcb4d9c99483b479534beb22144ad31" + "reference": "864c03ae1627854f274b02ff91cfb35e2db08c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-module-adfs/zipball/ccb86deb3dcb4d9c99483b479534beb22144ad31", - "reference": "ccb86deb3dcb4d9c99483b479534beb22144ad31", + "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-module-adfs/zipball/864c03ae1627854f274b02ff91cfb35e2db08c1b", + "reference": "864c03ae1627854f274b02ff91cfb35e2db08c1b", "shasum": "" }, "require": { @@ -6142,7 +6141,7 @@ "simplesamlphp/xml-security": "~0.4.1" }, "require-dev": { - "simplesamlphp/simplesamlphp": "^2.0.0-beta.5", + "simplesamlphp/simplesamlphp": "^2.0.0-beta.9", "simplesamlphp/simplesamlphp-test-framework": "^1.1.6" }, "type": "simplesamlphp-module", @@ -6170,7 +6169,7 @@ "issues": "https://github.com/simplesamlphp/simplesamlphp-module-adfs/issues", "source": "https://github.com/simplesamlphp/simplesamlphp-module-adfs" }, - "time": "2022-03-16T16:39:11+00:00" + "time": "2022-04-08T10:07:24+00:00" }, { "name": "simplesamlphp/simplesamlphp-test-framework", @@ -6231,16 +6230,16 @@ }, { "name": "simplesamlphp/xml-common", - "version": "v0.8.7", + "version": "v0.8.8", "source": { "type": "git", "url": "https://github.com/simplesamlphp/xml-common.git", - "reference": "de0bdbd52c0f7545b659308b08a37fd8655e6b42" + "reference": "b8774b31f9f5a2c4e3c7c37d5eafe3cc4aed2687" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/xml-common/zipball/de0bdbd52c0f7545b659308b08a37fd8655e6b42", - "reference": "de0bdbd52c0f7545b659308b08a37fd8655e6b42", + "url": "https://api.github.com/repos/simplesamlphp/xml-common/zipball/b8774b31f9f5a2c4e3c7c37d5eafe3cc4aed2687", + "reference": "b8774b31f9f5a2c4e3c7c37d5eafe3cc4aed2687", "shasum": "" }, "require": { @@ -6285,20 +6284,20 @@ "issues": "https://github.com/simplesamlphp/xml-common/issues", "source": "https://github.com/simplesamlphp/xml-common" }, - "time": "2022-02-17T22:50:21+00:00" + "time": "2022-04-08T13:37:27+00:00" }, { "name": "simplesamlphp/xml-security", - "version": "v0.4.2", + "version": "v0.4.3", "source": { "type": "git", "url": "https://github.com/simplesamlphp/xml-security.git", - "reference": "f6640954363b94beb59b0001110b647dcb3a8fa3" + "reference": "1d8fc035a06c2be7bfd80b5252a15f4e060fe75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/xml-security/zipball/f6640954363b94beb59b0001110b647dcb3a8fa3", - "reference": "f6640954363b94beb59b0001110b647dcb3a8fa3", + "url": "https://api.github.com/repos/simplesamlphp/xml-security/zipball/1d8fc035a06c2be7bfd80b5252a15f4e060fe75c", + "reference": "1d8fc035a06c2be7bfd80b5252a15f4e060fe75c", "shasum": "" }, "require": { @@ -6307,11 +6306,11 @@ "ext-spl": "*", "php": ">= 7.4 || ^8.0", "robrichards/xmlseclibs": "^3.1.1", - "simplesamlphp/assert": "~0.2.12", - "simplesamlphp/xml-common": "^0.8.5" + "simplesamlphp/assert": "~0.2.13", + "simplesamlphp/xml-common": "^0.8.8" }, "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "^1.1.6" + "simplesamlphp/simplesamlphp-test-framework": "^1.1.7" }, "type": "library", "autoload": { @@ -6345,9 +6344,9 @@ ], "support": { "issues": "https://github.com/simplesamlphp/xml-security/issues", - "source": "https://github.com/simplesamlphp/xml-security/tree/v0.4.2" + "source": "https://github.com/simplesamlphp/xml-security/tree/v0.4.3" }, - "time": "2022-02-16T17:17:35+00:00" + "time": "2022-04-08T18:57:08+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -6407,16 +6406,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v6.0.3", + "version": "v6.0.7", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "81f5e8e453433e0182a49ca45d4734cb3a2f818f" + "reference": "924f44f1c682473453a502f8f01d4904a7761dcc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/81f5e8e453433e0182a49ca45d4734cb3a2f818f", - "reference": "81f5e8e453433e0182a49ca45d4734cb3a2f818f", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/924f44f1c682473453a502f8f01d4904a7761dcc", + "reference": "924f44f1c682473453a502f8f01d4904a7761dcc", "shasum": "" }, "require": { @@ -6470,7 +6469,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.0.3" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.0.7" }, "funding": [ { @@ -6486,7 +6485,7 @@ "type": "tidelift" } ], - "time": "2022-01-26T17:23:29+00:00" + "time": "2022-03-06T11:27:28+00:00" }, { "name": "theseer/tokenizer", diff --git a/docs/simplesamlphp-artifact-idp.md b/docs/simplesamlphp-artifact-idp.md index fcbf0079d..75286aeaf 100644 --- a/docs/simplesamlphp-artifact-idp.md +++ b/docs/simplesamlphp-artifact-idp.md @@ -68,12 +68,12 @@ In general, that should look something like: 'AssertionConsumerService' => array ( [ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', + 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', 'index' => 0, ], [ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact', - 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', + 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', 'index' => 2, ], ), diff --git a/docs/simplesamlphp-hok-idp.md b/docs/simplesamlphp-hok-idp.md index a2315bf74..7bf375e1b 100644 --- a/docs/simplesamlphp-hok-idp.md +++ b/docs/simplesamlphp-hok-idp.md @@ -64,12 +64,12 @@ In general, this should look like the following code: 'AssertionConsumerService' => array ( [ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', + 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', 'index' => 0, ], [ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', - 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', + 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', 'index' => 4, ], ), diff --git a/docs/simplesamlphp-idp.md b/docs/simplesamlphp-idp.md index 101d57076..a4f623fdf 100644 --- a/docs/simplesamlphp-idp.md +++ b/docs/simplesamlphp-idp.md @@ -191,8 +191,8 @@ This is a minimal example of a `metadata/saml20-sp-remote.php` metadata file for <?php $metadata['https://sp.example.org/simplesaml/module.php/saml/sp/metadata.php/default-sp'] = [ - 'AssertionConsumerService' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', - 'SingleLogoutService' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', + 'AssertionConsumerService' => 'https://sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', + 'SingleLogoutService' => 'https://sp.example.org/simplesaml/module.php/saml/sp/singleLogoutService/default-sp', ]; Note that the URI in the entityID and the URLs to the AssertionConsumerService and SingleLogoutService endpoints change between different service providers. diff --git a/lib/SimpleSAML/Configuration.php b/lib/SimpleSAML/Configuration.php index 23587bfa0..1bb5eaeed 100644 --- a/lib/SimpleSAML/Configuration.php +++ b/lib/SimpleSAML/Configuration.php @@ -1015,7 +1015,8 @@ class Configuration implements Utils\ClearableState * @param string[]|null $default A default value which will be returned if the option isn't found. * The default value can be null or an array of strings. * - * @return string[]|null The option with the given name, or $default if the option isn't found and $default is specified. + * @return string[]|null The option with the given name, or $default if the option isn't found + * and $default is specified. * @psalm-return ($default is set ? array|null : array) * * @throws \SimpleSAML\Assert\AssertionFailedException If the option is not a string or an array of strings. @@ -1082,7 +1083,10 @@ class Configuration implements Utils\ClearableState { $ret = $this->getOptionalArray($name, $default); - return ($ret === null) ? null : self::loadFromArray($ret, $this->location . '[' . var_export($name, true) . ']'); + if ($ret !== null) { + return self::loadFromArray($ret, $this->location . '[' . var_export($name, true) . ']'); + } + return null; } diff --git a/lib/SimpleSAML/Database.php b/lib/SimpleSAML/Database.php index 239b88eb6..1c2105fa7 100644 --- a/lib/SimpleSAML/Database.php +++ b/lib/SimpleSAML/Database.php @@ -105,7 +105,10 @@ class Database ); } // connect to any configured secondaries, preserving legacy config option - $secondaries = $config->getOptionalArray('database.secondaries', $config->getOptionalArray('database.slaves', [])); + $secondaries = $config->getOptionalArray( + 'database.secondaries', + $config->getOptionalArray('database.slaves', []) + ); foreach ($secondaries as $secondary) { array_push( $this->dbSecondaries, @@ -140,7 +143,10 @@ class Database ], // TODO: deprecated: the "database.slave" terminology is preserved here for backwards compatibility. - 'secondaries' => $config->getOptionalArray('database.secondaries', $config->getOptionalArray('database.slaves', [])), + 'secondaries' => $config->getOptionalArray( + 'database.secondaries', + $config->getOptionalArray('database.slaves', []) + ), ]; return sha1(serialize($assembledConfig)); diff --git a/lib/SimpleSAML/Logger/SyslogLoggingHandler.php b/lib/SimpleSAML/Logger/SyslogLoggingHandler.php index 40a0923f6..1bff3034a 100644 --- a/lib/SimpleSAML/Logger/SyslogLoggingHandler.php +++ b/lib/SimpleSAML/Logger/SyslogLoggingHandler.php @@ -27,7 +27,10 @@ class SyslogLoggingHandler implements LoggingHandlerInterface */ public function __construct(Configuration $config) { - $facility = $config->getOptionalInteger('logging.facility', defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER); + $facility = $config->getOptionalInteger( + 'logging.facility', + defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER + ); // Remove any non-printable characters before storing $processname = preg_replace( diff --git a/lib/SimpleSAML/XHTML/IdPDisco.php b/lib/SimpleSAML/XHTML/IdPDisco.php index 852a2d6de..d141f48d2 100644 --- a/lib/SimpleSAML/XHTML/IdPDisco.php +++ b/lib/SimpleSAML/XHTML/IdPDisco.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SimpleSAML\XHTML; +use Exception; use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Logger; @@ -11,6 +12,19 @@ use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Session; use SimpleSAML\Utils; +use function array_fill_keys; +use function array_intersect_key; +use function array_intersect; +use function array_key_exists; +use function array_keys; +use function array_merge; +use function htmlspecialchars; +use function preg_match; +use function sizeof; +use function strcasecmp; +use function urldecode; +use function usort; + /** * This class implements a generic IdP discovery service, for use in various IdP * discovery service pages. This should reduce code duplication. @@ -128,7 +142,7 @@ class IdPDisco // standard discovery service parameters if (!array_key_exists('entityID', $_GET)) { - throw new \Exception('Missing parameter: entityID'); + throw new Exception('Missing parameter: entityID'); } else { $this->spEntityId = $_GET['entityID']; } @@ -142,7 +156,7 @@ class IdPDisco $this->log('returnIdParam initially set to [' . $this->returnIdParam . ']'); if (!array_key_exists('return', $_GET)) { - throw new \Exception('Missing parameter: return'); + throw new Exception('Missing parameter: return'); } else { $httpUtils = new Utils\HTTP(); $this->returnURL = $httpUtils->checkURLAllowed($_GET['return']); @@ -251,7 +265,7 @@ class IdPDisco try { $this->metadata->getMetaData($idp, $metadataSet); return $idp; - } catch (\Exception $e) { + } catch (Exception $e) { // continue } } @@ -584,7 +598,7 @@ class IdPDisco $templateFile = 'selectidp-links.twig'; break; default: - throw new \Exception('Invalid value for the \'idpdisco.layout\' option.'); + throw new Exception('Invalid value for the \'idpdisco.layout\' option.'); } $t = new Template($this->config, $templateFile); diff --git a/metadata-templates/saml20-sp-remote.php b/metadata-templates/saml20-sp-remote.php index ef2f81e96..2cf2873ca 100644 --- a/metadata-templates/saml20-sp-remote.php +++ b/metadata-templates/saml20-sp-remote.php @@ -10,8 +10,8 @@ * Example SimpleSAMLphp SAML 2.0 SP */ $metadata['https://saml2sp.example.org'] = [ - 'AssertionConsumerService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', - 'SingleLogoutService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', + 'AssertionConsumerService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', + 'SingleLogoutService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/singleLogoutService/default-sp', ]; /* diff --git a/modules/admin/lib/Controller/Federation.php b/modules/admin/lib/Controller/Federation.php index 6969ca17d..6b8e08a15 100644 --- a/modules/admin/lib/Controller/Federation.php +++ b/modules/admin/lib/Controller/Federation.php @@ -331,7 +331,10 @@ class Federation // get the name $name = $source->getMetadata()->getOptionalLocalizedString( 'name', - $source->getMetadata()->getOptionalLocalizedString('OrganizationDisplayName', ['en' => $source->getAuthId()]) + $source->getMetadata()->getOptionalLocalizedString( + 'OrganizationDisplayName', + ['en' => $source->getAuthId()] + ) ); $builder = new SAMLBuilder($source->getEntityId()); diff --git a/modules/core/docs/authproc_attributelimit.md b/modules/core/docs/authproc_attributelimit.md index 220fd54d7..90bf2a738 100644 --- a/modules/core/docs/authproc_attributelimit.md +++ b/modules/core/docs/authproc_attributelimit.md @@ -86,8 +86,8 @@ like this: Then, add the allowed attributes to each service provider metadata, in the `attributes` option: $metadata['https://saml2sp.example.org'] = [ - 'AssertionConsumerService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', - 'SingleLogoutService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', + 'AssertionConsumerService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', + 'SingleLogoutService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/singleLogoutService/default-sp', ... 'attributes' => ['cn', 'mail'], ... @@ -97,8 +97,8 @@ Now, let's look to a couple of examples on how to filter out attribute values. F to be used by a service provider (among other attributes): $metadata['https://saml2sp.example.org'] = [ - 'AssertionConsumerService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', - 'SingleLogoutService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', + 'AssertionConsumerService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/assertionConsumerService/default-sp', + 'SingleLogoutService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/singleLogoutService/default-sp', ... 'attributes' => [ 'uid', diff --git a/modules/core/lib/Auth/Process/AttributeAlter.php b/modules/core/lib/Auth/Process/AttributeAlter.php index 6b851d7a0..fb3551a0c 100644 --- a/modules/core/lib/Auth/Process/AttributeAlter.php +++ b/modules/core/lib/Auth/Process/AttributeAlter.php @@ -163,7 +163,7 @@ class AttributeAlter extends Auth\ProcessingFilter if ($this->subject === $this->target) { $value = $new_value; - } else if ($this->merge === true) { + } elseif ($this->merge === true) { $attributes[$this->target] = array_values( array_diff($attributes[$this->target], [$value]) ); diff --git a/modules/core/lib/Controller/Logout.php b/modules/core/lib/Controller/Logout.php index 6b1bbf52d..8adcb5876 100644 --- a/modules/core/lib/Controller/Logout.php +++ b/modules/core/lib/Controller/Logout.php @@ -310,7 +310,8 @@ class Logout if (method_exists($sp['Handler'], 'getAssociationConfig')) { $assocIdP = IdP::getByState($sp); $assocConfig = call_user_func([$sp['Handler'], 'getAssociationConfig'], $assocIdP, $sp); - $sp['core:Logout-IFrame:Timeout'] = $assocConfig->getOptionalInteger('core:logout-timeout', 5) + time(); + $timeout = $assocConfig->getOptionalInteger('core:logout-timeout', 5); + $sp['core:Logout-IFrame:Timeout'] = $timeout + time(); } else { $sp['core:Logout-IFrame:Timeout'] = time() + 5; } diff --git a/modules/saml/lib/Auth/Source/SP.php b/modules/saml/lib/Auth/Source/SP.php index e17726f5a..c7b5a0d12 100644 --- a/modules/saml/lib/Auth/Source/SP.php +++ b/modules/saml/lib/Auth/Source/SP.php @@ -179,7 +179,10 @@ class SP extends \SimpleSAML\Auth\Source $org = $this->metadata->getOptionalLocalizedString('OrganizationName', null); if ($org !== null) { $metadata['OrganizationName'] = $org; - $metadata['OrganizationDisplayName'] = $this->metadata->getOptionalLocalizedString('OrganizationDisplayName', $org); + $metadata['OrganizationDisplayName'] = $this->metadata->getOptionalLocalizedString( + 'OrganizationDisplayName', + $org + ); $metadata['OrganizationURL'] = $this->metadata->getOptionalLocalizedString('OrganizationURL', null); if ($metadata['OrganizationURL'] === null) { throw new Error\Exception( @@ -350,19 +353,19 @@ class SP extends \SimpleSAML\Auth\Source case Constants::BINDING_HTTP_POST: $acs = [ 'Binding' => Constants::BINDING_HTTP_POST, - 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + 'Location' => Module::getModuleURL('saml/sp/assertionConsumerService/' . $this->getAuthId()), ]; break; case Constants::BINDING_HTTP_ARTIFACT: $acs = [ 'Binding' => Constants::BINDING_HTTP_ARTIFACT, - 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + 'Location' => Module::getModuleURL('saml/sp/assertionConsumerService/' . $this->getAuthId()), ]; break; case Constants::BINDING_HOK_SSO: $acs = [ 'Binding' => Constants::BINDING_HOK_SSO, - 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + 'Location' => Module::getModuleURL('saml/sp/assertionConsumerService/' . $this->getAuthId()), 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT, ]; break; @@ -397,7 +400,7 @@ class SP extends \SimpleSAML\Auth\Source Constants::BINDING_SOAP, ] ); - $defaultLocation = Module::getModuleURL('saml/sp/saml2-logout.php/' . $this->getAuthId()); + $defaultLocation = Module::getModuleURL('saml/sp/singleLogoutService/' . $this->getAuthId()); $location = $this->metadata->getOptionalString('SingleLogoutServiceLocation', $defaultLocation); $endpoints = []; @@ -432,7 +435,7 @@ class SP extends \SimpleSAML\Auth\Source $ar = Module\saml\Message::buildAuthnRequest($this->metadata, $idpMetadata); - $ar->setAssertionConsumerServiceURL(Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->authId)); + $ar->setAssertionConsumerServiceURL(Module::getModuleURL('saml/sp/assertionConsumerService/' . $this->authId)); if (isset($state['\SimpleSAML\Auth\Source.ReturnURL'])) { $ar->setRelayState($state['\SimpleSAML\Auth\Source.ReturnURL']); @@ -686,10 +689,10 @@ class SP extends \SimpleSAML\Auth\Source $discoURL = $this->discoURL; if ($discoURL === null) { // Fallback to internal discovery service - $discoURL = Module::getModuleURL('saml/disco.php'); + $discoURL = Module::getModuleURL('saml/disco'); } - $returnTo = Module::getModuleURL('saml/sp/discoresp.php', ['AuthID' => $id]); + $returnTo = Module::getModuleURL('saml/sp/discoResponse', ['AuthID' => $id]); $params = [ 'entityID' => $this->entityId, diff --git a/modules/saml/lib/Controller/Disco.php b/modules/saml/lib/Controller/Disco.php new file mode 100644 index 000000000..24cf78a3e --- /dev/null +++ b/modules/saml/lib/Controller/Disco.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\saml\Controller; + +use SimpleSAML\Configuration; +use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\XHTML\IdPDisco; +use Symfony\Component\HttpFoundation\Request; + +/** + * Controller class for the saml module. + * + * This class serves the different views available in the module. + * + * @package simplesamlphp/simplesamlphp + */ +class Disco +{ + /** @var \SimpleSAML\Configuration */ + protected Configuration $config; + + + /** + * Controller constructor. + * + * It initializes the global configuration for the controllers implemented here. + * + * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. + */ + public function __construct( + Configuration $config + ) { + $this->config = $config; + } + + + /** + * Built-in IdP discovery service + * + * @return \SimpleSAML\Http\RunnableResponse + */ + public function disco(): RunnableResponse + { + $disco = new IdPDisco(['saml20-idp-remote'], 'saml'); + return new RunnableResponse([$disco, 'handleRequest']); + } +} diff --git a/modules/saml/lib/Controller/Proxy.php b/modules/saml/lib/Controller/Proxy.php new file mode 100644 index 000000000..a02a29f7d --- /dev/null +++ b/modules/saml/lib/Controller/Proxy.php @@ -0,0 +1,122 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\saml\Controller; + +use Exception; +use SAML2\Constants; +use SimpleSAML\Assert\Assert; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\IdP; +use SimpleSAML\Module\saml\Auth\Source\SP; +use SimpleSAML\Module\saml\Error\NoAvailableIDP; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\{Request, Response}; + +/** + * Controller class for the saml module. + * + * This class serves the different views available in the module. + * + * @package simplesamlphp/simplesamlphp + */ +class Proxy +{ + /** @var \SimpleSAML\Configuration */ + protected Configuration $config; + + /** + * @var \SimpleSAML\Auth\State|string + * @psalm-var \SimpleSAML\Auth\State|class-string + */ + protected $authState = Auth\State::class; + + + /** + * Controller constructor. + * + * It initializes the global configuration for the controllers implemented here. + * + * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. + */ + public function __construct( + Configuration $config + ) { + $this->config = $config; + } + + + /** + * Inject the \SimpleSAML\Auth\State dependency. + * + * @param \SimpleSAML\Auth\State $authState + */ + public function setAuthState(Auth\State $authState): void + { + $this->authState = $authState; + } + + + /** + * This controller will handle the case of a user with an existing session that's not valid for a specific + * Service Provider, since the authenticating IdP is not in the list of IdPs allowed by the SP. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @return \SimpleSAML\XHTML\Template|\Symfony\Component\HttpFoundation\Response + */ + public function invalidSession(Request $request): Response + { + // retrieve the authentication state + if (!$request->query->has('AuthState')) { + throw new Error\BadRequest('Missing mandatory parameter: AuthState'); + } + $stateId = $request->query->get('AuthState'); + + try { + // try to get the state + $state = $this->authState::loadState($stateId, 'saml:proxy:invalid_idp'); + } catch (Exception $e) { + // the user probably hit the back button after starting the logout, + // try to recover the state with another stage + $state = $this->authState::loadState($stateId, 'core:Logout:afterbridge'); + + // success! Try to continue with reauthentication, since we no longer have a valid session here + $idp = IdP::getById($state['core:IdP']); + return new RunnableResponse([SP::class, 'reauthPostLogout'], [$idp, $state]); + } + + if ($request->request->has('cancel')) { + // the user does not want to logout, cancel login + $this->authState::throwException( + $state, + new NoAvailableIDP( + Constants::STATUS_RESPONDER, + 'User refused to reauthenticate with any of the IdPs requested.' + ) + ); + } + + if ($request->request->has('continue')) { + /** @var \SimpleSAML\Module\saml\Auth\Source\SP $as */ + $as = Auth\Source::getById($state['saml:sp:AuthId'], SP::class); + + // log the user out before being able to login again + return new RunnableResponse([$as, 'reauthLogout'], [$state]); + } + + $template = new Template($this->config, 'saml:proxy/invalid_session.twig'); + $template->data['AuthState'] = $stateId; + + /** @var \SimpleSAML\Configuration $idpmdcfg */ + $idpmdcfg = $state['saml:sp:IdPMetadata']; + + $template->data['entity_idp'] = $idpmdcfg->toArray(); + $template->data['entity_sp'] = $state['SPMetadata']; + + return $template; + } +} diff --git a/modules/saml/lib/Controller/ServiceProvider.php b/modules/saml/lib/Controller/ServiceProvider.php new file mode 100644 index 000000000..bf48408ae --- /dev/null +++ b/modules/saml/lib/Controller/ServiceProvider.php @@ -0,0 +1,613 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\saml\Controller; + +use Exception; +use SAML2\Assertion; +use SAML2\Binding; +use SAML2\Constants; +use SAML2\Exception\Protocol\UnsupportedBindingException; +use SAML2\HTTPArtifact; +use SAML2\LogoutRequest; +use SAML2\LogoutResponse; +use SAML2\Response as SAML2_Response; +use SAML2\SOAP; +use SAML2\XML\saml\Issuer; +use SimpleSAML\Assert\Assert; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\Logger; +use SimpleSAML\Metadata; +use SimpleSAML\Module; +use SimpleSAML\Module\saml\Auth\Source\SP; +use SimpleSAML\Session; +use SimpleSAML\Store\StoreFactory; +use SimpleSAML\Utils; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\{Request, Response}; + +use function array_merge; +use function count; +use function end; +use function get_class; +use function in_array; +use function is_null; +use function substr; +use function time; +use function var_export; + +/** + * Controller class for the saml module. + * + * This class serves the different views available in the module. + * + * @package simplesamlphp/simplesamlphp + */ +class ServiceProvider +{ + /** @var \SimpleSAML\Configuration */ + protected Configuration $config; + + /** @var \SimpleSAML\Session */ + protected Session $session; + + /** + * @var \SimpleSAML\Auth\State|string + * @psalm-var \SimpleSAML\Auth\State|class-string + */ + protected $authState = Auth\State::class; + + /** @var \SimpleSAML\Utils\Auth */ + protected Utils\Auth $authUtils; + + + /** + * Controller constructor. + * + * It initializes the global configuration for the controllers implemented here. + * + * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. + * @param \SimpleSAML\Session $session The Session to use by the controllers. + */ + public function __construct( + Configuration $config, + Session $session + ) { + $this->config = $config; + $this->session = $session; + $this->authUtils = new Utils\Auth(); + } + + + /** + * Inject the \SimpleSAML\Auth\State dependency. + * + * @param \SimpleSAML\Auth\State $authState + */ + public function setAuthState(Auth\State $authState): void + { + $this->authState = $authState; + } + + + /** + * Inject the \SimpleSAML\Utils\Auth dependency. + * + * @param \SimpleSAML\Utils\Auth $authUtils + */ + public function setAuthUtils(Utils\Auth $authUtils): void + { + $this->authUtils = $authUtils; + } + + + /** + * Handler for response from IdP discovery service. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @return \SimpleSAML\Http\RunnableResponse + */ + public function discoResponse(Request $request): RunnableResponse + { + if (!$request->query->has('AuthID')) { + throw new Error\BadRequest('Missing AuthID to discovery service response handler'); + } + $authId = $request->query->get('AuthID'); + + if (!$request->query->has('idpentityid')) { + throw new Error\BadRequest('Missing idpentityid to discovery service response handler'); + } + $idpEntityId = $request->query->get('idpentityid'); + + $state = $this->authState::loadState($authId, 'saml:sp:sso'); + + // Find authentication source + Assert::keyExists($state, 'saml:sp:AuthId'); + $sourceId = $state['saml:sp:AuthId']; + + $source = Auth\Source::getById($sourceId); + if ($source === null) { + throw new Exception('Could not find authentication source with id ' . $sourceId); + } + + if (!($source instanceof SP)) { + throw new Error\Exception('Source type changed?'); + } + + return new RunnableResponse([$source, 'startSSO'], [$idpEntityId, $state]); + } + + + /** + * @return \SimpleSAML\XHTML\Template + */ + public function wrongAuthnContextClassRef(): Template + { + return new Template($this->config, 'saml:sp/wrong_authncontextclassref.twig'); + } + + + /** + * Handler for the Assertion Consumer Service. + * + * @param string $sourceId + * @return \SimpleSAML\Http\RunnableResponse + */ + public function assertionConsumerService(string $sourceId): RunnableResponse + { + /** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */ + $source = Auth\Source::getById($sourceId, SP::class); + + $spMetadata = $source->getMetadata(); + try { + $b = Binding::getCurrentBinding(); + } catch (UnsupportedBindingException $e) { + throw new Error\Error('ACSPARAMS', $e, 400); + } + + if ($b instanceof HTTPArtifact) { + $b->setSPMetadata($spMetadata); + } + + $response = $b->receive(); + if (!($response instanceof SAML2_Response)) { + throw new Error\BadRequest('Invalid message received at AssertionConsumerService endpoint.'); + } + + $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 Assertion) { + // we found an unencrypted assertion, there should be an issuer here + $issuer = $a->getIssuer(); + break; + } + } + if ($issuer === null) { + // no issuer found in the assertions + throw new Exception('Missing <saml:Issuer> in message delivered to AssertionConsumerService.'); + } + } + $issuer = $issuer->getValue(); + + $prevAuth = $this->session->getAuthData($sourceId, 'saml:sp:prevAuth'); + + $httpUtils = new Utils\HTTP(); + 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. + */ + Logger::info( + 'Duplicate SAML 2 response detected - ignoring the response and redirecting the user to the correct page.' + ); + if (isset($prevAuth['redirect'])) { + return new RunnableResponse([$httpUtils, 'redirectTrustedURL'], [$prevAuth['redirect']]); + } + + Logger::info('No RelayState or ReturnURL available, cannot redirect.'); + throw new 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 = $this->authState::loadState($stateId, 'saml:sp:sso'); + } catch (Exception $e) { + // something went wrong, + Logger::warning(sprintf( + 'Could not load state specified by InResponseTo: %s Processing response as unsolicited.', + $e->getMessage(), + )); + } + } + + $enableUnsolicited = $spMetadata->getOptionalBoolean('enable_unsolicited', true); + if ($state === null && $enableUnsolicited === false) { + throw new Error\BadRequest('Unsolicited responses are denied by configuration.'); + } + + if ($state) { + // check that the authentication source is correct + Assert::keyExists($state, 'saml:sp:AuthId'); + if ($state['saml:sp:AuthId'] !== $sourceId) { + throw new 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::keyExists($state, 'ExpectedIssuer'); + if ($state['ExpectedIssuer'] !== $issuer) { + $idpMetadata = $source->getIdPMetadata($issuer); + $idplist = $idpMetadata->getOptionalArrayize('IDPList', []); + if (!in_array($state['ExpectedIssuer'], $idplist, true)) { + 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->getOptionalString('RelayState', $response->getRelayState()); + $state = [ + 'saml:sp:isUnsolicited' => true, + 'saml:sp:AuthId' => $sourceId, + 'saml:sp:RelayState' => $relaystate === null ? null : $httpUtils->checkURLAllowed($relaystate), + ]; + } + + Logger::debug('Received SAML2 Response from ' . var_export($issuer, true) . '.'); + + if (is_null($idpMetadata)) { + $idpMetadata = $source->getIdPmetadata($issuer); + } + + try { + $assertions = Module\saml\Message::processResponse($spMetadata, $idpMetadata, $response); + } catch (Module\saml\Error $e) { + // the status of the response wasn't "success" + $e = $e->toException(); + $this->authState::throwException($state, $e); + Assert::true(false); + } + + $authenticatingAuthority = null; + $nameId = null; + $sessionIndex = null; + $expire = null; + $attributes = []; + $foundAuthnStatement = false; + + $storeType = $this->config->getOptionalString('store.type', 'phpsession'); + + $store = StoreFactory::getInstance($storeType); + + foreach ($assertions as $assertion) { + // check for duplicate assertion (replay attack) + if ($store !== false) { + $aID = $assertion->getId(); + if ($store->get('saml.AssertionReceived', $aID) !== null) { + $e = new Error\Exception('Received duplicate assertion.'); + $this->authState::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 Error\Exception('No AuthnStatement found in assertion(s).'); + $this->authState::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 + 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'; + + return new RunnableResponse([$source, 'handleResponse'], [$state, $issuer, $attributes]); + } + + + /** + * Logout endpoint handler for SAML SP authentication client. + * + * This endpoint handles both logout requests and logout responses. + * + * @param string $sourceId + * @return \SimpleSAML\Http\RunnableResponse + */ + public function singleLogoutService(string $sourceId): RunnableResponse + { + /** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */ + $source = Auth\Source::getById($sourceId); + + if ($source === null) { + throw new Error\Exception('No authentication source with id \'' . $sourceId . '\' found.'); + } elseif (!($source instanceof \SimpleSAML\Module\saml\Auth\Source\SP)) { + throw new Error\Exception('Source type changed?'); + } + + try { + $binding = Binding::getCurrentBinding(); + } catch (UnsupportedBindingException $e) { + throw new Error\Error('SLOSERVICEPARAMS', $e, 400); + } + $message = $binding->receive(); + + $issuer = $message->getIssuer(); + if ($issuer instanceof Issuer) { + $idpEntityId = $issuer->getValue(); + } else { + $idpEntityId = $issuer; + } + + if ($idpEntityId === null) { + // Without an issuer we have no way to respond to the message. + throw new Error\BadRequest('Received message on logout endpoint without issuer.'); + } + + $spEntityId = $source->getEntityId(); + + $idpMetadata = $source->getIdPMetadata($idpEntityId); + $spMetadata = $source->getMetadata(); + + Module\saml\Message::validateMessage($idpMetadata, $spMetadata, $message); + + $httpUtils = new Utils\HTTP(); + $destination = $message->getDestination(); + if ($destination !== null && $destination !== $httpUtils->getSelfURLNoQuery()) { + throw new Error\Exception('Destination in logout message is wrong.'); + } + + if ($message instanceof LogoutResponse) { + $relayState = $message->getRelayState(); + if ($relayState === null) { + // Somehow, our RelayState has been lost. + throw new Error\BadRequest('Missing RelayState in logout response.'); + } + + if (!$message->isSuccess()) { + Logger::warning( + 'Unsuccessful logout. Status was: ' . Module\saml\Message::getResponseError($message) + ); + } + + $state = $this->authState::loadState($relayState, 'saml:slosent'); + $state['saml:sp:LogoutStatus'] = $message->getStatus(); + return new RunnableResponse([Auth\Source::class, 'completeLogout'], [$state]); + } elseif ($message instanceof LogoutRequest) { + Logger::debug('module/saml2/sp/logout: Request from ' . $idpEntityId); + Logger::stats('saml20-idp-SLO idpinit ' . $spEntityId . ' ' . $idpEntityId); + + if ($message->isNameIdEncrypted()) { + try { + $keys = Module\saml\Message::getDecryptionKeys($idpMetadata, $spMetadata); + } catch (Exception $e) { + throw new Error\Exception('Error decrypting NameID: ' . $e->getMessage()); + } + + $blacklist = Module\saml\Message::getBlacklistedAlgorithms($idpMetadata, $spMetadata); + + $lastException = null; + foreach ($keys as $i => $key) { + try { + $message->decryptNameId($key, $blacklist); + Logger::debug('Decryption with key #' . $i . ' succeeded.'); + $lastException = null; + break; + } catch (Exception $e) { + 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 = 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 = Module\saml\Message::buildLogoutResponse($spMetadata, $idpMetadata); + $lr->setRelayState($message->getRelayState()); + $lr->setInResponseTo($message->getId()); + + if ($numLoggedOut < count($sessionIndexes)) { + Logger::warning('Logged out of ' . $numLoggedOut . ' of ' . count($sessionIndexes) . ' sessions.'); + } + + $dst = $idpMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ] + ); + + if (!($binding instanceof SOAP)) { + $binding = Binding::getBinding($dst['Binding']); + if (isset($dst['ResponseLocation'])) { + $dst = $dst['ResponseLocation']; + } else { + $dst = $dst['Location']; + } + $binding->setDestination($dst); + } else { + $lr->setDestination($dst['Location']); + } + + return new RunnableResponse([$binding, 'send'], [$lr]); + } else { + throw new Error\BadRequest('Unknown message received on logout endpoint: ' . get_class($message)); + } + } + + + /** + * Metadata endpoint for SAML SP + * + * @param string $sourceId + * @return \Symfony\Component\HttpFoundation\Response|\SimpleSAML\HTTP\RunnableResponse + */ + public function metadata(string $sourceId): Response + { + if ($this->config->getOptionalBoolean('admin.protectmetadata', false)) { + return new RunnableResponse([$this->authUtils, 'requireAdmin']); + } + + $source = Auth\Source::getById($sourceId); + if ($source === null) { + throw new Error\AuthSource($sourceId, 'Could not find authentication source.'); + } + + if (!($source instanceof SP)) { + throw new Error\AuthSource( + $sourceId, + 'The authentication source is not a SAML Service Provider.' + ); + } + + $entityId = $source->getEntityId(); + $spconfig = $source->getMetadata(); + $metaArray20 = $source->getHostedMetadata(); + + $metaBuilder = new Metadata\SAMLBuilder($entityId); + $metaBuilder->addMetadataSP20($metaArray20, $source->getSupportedProtocols()); + $metaBuilder->addOrganizationInfo($metaArray20); + + $xml = $metaBuilder->getEntityDescriptorText(); + + // sign the metadata if enabled + $metaxml = Metadata\Signer::sign($xml, $spconfig->toArray(), 'SAML 2 SP'); + + // make sure to export only the md:EntityDescriptor + $i = strpos($metaxml, '<md:EntityDescriptor'); + $metaxml = substr($metaxml, $i ? $i : 0); + + // 22 = strlen('</md:EntityDescriptor>') + $i = strrpos($metaxml, '</md:EntityDescriptor>'); + $metaxml = substr($metaxml, 0, $i ? $i + 22 : 0); + + $response = new Response(); + $response->headers->set('Content-Type', 'application/samlmetadata+xml'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . basename($sourceId) . '.xml"'); + $response->setContent($metaxml); + + return $response; + } +} diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php index da86d9c1c..3ed49b320 100644 --- a/modules/saml/lib/IdP/SAML2.php +++ b/modules/saml/lib/IdP/SAML2.php @@ -255,7 +255,10 @@ class SAML2 $skipEndpointValidation = false; if ($authnRequestSigned === true) { - $skipEndpointValidationWhenSigned = $spMetadata->getOptionalValue('skipEndpointValidationWhenSigned', false); + $skipEndpointValidationWhenSigned = $spMetadata->getOptionalValue( + 'skipEndpointValidationWhenSigned', + false + ); if (is_bool($skipEndpointValidationWhenSigned) === true) { $skipEndpointValidation = $skipEndpointValidationWhenSigned; } elseif (is_callable($skipEndpointValidationWhenSigned) === true) { diff --git a/modules/saml/routing/routes/routes.yaml b/modules/saml/routing/routes/routes.yaml new file mode 100644 index 000000000..1361b4cdf --- /dev/null +++ b/modules/saml/routing/routes/routes.yaml @@ -0,0 +1,30 @@ +saml-proxy-invalidSession: + path: /proxy/invalidSession + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\Proxy::invalidSession' } +saml-disco: + path: /disco + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\Disco::disco' } +saml-sp-discoResponse: + path: /sp/discoResponse + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::discoResponse' } +saml-sp-wrongAuthnContextClassRef: + path: /sp/wrongAuthnContextClassRef + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::wrongAuthnContextClassRef' } +saml-sp-assertionConsumerService: + path: /sp/assertionConsumerService/{sourceId} + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::assertionConsumerService' } +saml-sp-singleLogoutService: + path: /sp/singleLogoutService/{sourceId} + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::singleLogoutService' } +saml-sp-metadata: + path: /sp/metadata/{sourceId} + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::metadata' } +saml-legacy-sp-assertionConsumerService: + path: /sp/saml2-acs.php/{sourceId} + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::assertionConsumerService', path: /saml/sp/assertionConsumerService, permanent: true } +saml-legacy-sp-singleLogoutService: + path: /sp/saml2-logout.php/{sourceId} + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::singleLogoutService', path: /saml/sp/singleLogoutService, permanent: true } +saml-legacy-sp-metadata: + path: /sp/metadata.php/{sourceId} + defaults: { _controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::metadata', path: /saml/sp/metadata, permanent: true } diff --git a/modules/saml/www/disco.php b/modules/saml/www/disco.php deleted file mode 100644 index 93d8f68bb..000000000 --- a/modules/saml/www/disco.php +++ /dev/null @@ -1,8 +0,0 @@ -<?php - -/** - * Built-in IdP discovery service. - */ - -$discoHandler = new \SimpleSAML\XHTML\IdPDisco(['saml20-idp-remote'], 'saml'); -$discoHandler->handleRequest(); diff --git a/modules/saml/www/proxy/invalid_session.php b/modules/saml/www/proxy/invalid_session.php deleted file mode 100644 index 5181da60e..000000000 --- a/modules/saml/www/proxy/invalid_session.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php - -/** - * This file will handle the case of a user with an existing session that's not valid for a specific Service Provider, - * since the authenticating IdP is not in the list of IdPs allowed by the SP. - * - * - * @package SimpleSAMLphp - */ - -// retrieve the authentication state -if (!array_key_exists('AuthState', $_REQUEST)) { - throw new \SimpleSAML\Error\BadRequest('Missing mandatory parameter: AuthState'); -} - -try { - // try to get the state - $state = \SimpleSAML\Auth\State::loadState($_REQUEST['AuthState'], 'saml:proxy:invalid_idp'); -} catch (\Exception $e) { - // the user probably hit the back button after starting the logout, try to recover the state with another stage - $state = \SimpleSAML\Auth\State::loadState($_REQUEST['AuthState'], 'core:Logout:afterbridge'); - - // success! Try to continue with reauthentication, since we no longer have a valid session here - $idp = \SimpleSAML\IdP::getById($state['core:IdP']); - \SimpleSAML\Module\saml\Auth\Source\SP::reauthPostLogout($idp, $state); -} - -if (isset($_POST['cancel'])) { - // the user does not want to logout, cancel login - \SimpleSAML\Auth\State::throwException( - $state, - new \SimpleSAML\Module\saml\Error\NoAvailableIDP( - \SAML2\Constants::STATUS_RESPONDER, - 'User refused to reauthenticate with any of the IdPs requested.' - ) - ); -} - -if (isset($_POST['continue'])) { - /** @var \SimpleSAML\Module\saml\Auth\Source\SP $as */ - $as = \SimpleSAML\Auth\Source::getById($state['saml:sp:AuthId'], '\SimpleSAML\Module\saml\Auth\Source\SP'); - - // log the user out before being able to login again - $as->reauthLogout($state); -} - -$cfg = \SimpleSAML\Configuration::getInstance(); -$template = new \SimpleSAML\XHTML\Template($cfg, 'saml:proxy/invalid_session.twig'); -$template->data['AuthState'] = (string) $_REQUEST['AuthState']; - -$idpmdcfg = $state['saml:sp:IdPMetadata']; -/** @var \SimpleSAML\Configuration $idpmdcfg */ -$template->data['entity_idp'] = $idpmdcfg->toArray(); -$template->data['entity_sp'] = $state['SPMetadata']; - -$template->send(); diff --git a/modules/saml/www/sp/discoresp.php b/modules/saml/www/sp/discoresp.php deleted file mode 100644 index 5944cfae5..000000000 --- a/modules/saml/www/sp/discoresp.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php - -/** - * Handler for response from IdP discovery service. - */ - -use SimpleSAML\Assert\Assert; -use SimpleSAML\Auth; -use SimpleSAML\Error; - -if (!array_key_exists('AuthID', $_REQUEST)) { - throw new Error\BadRequest('Missing AuthID to discovery service response handler'); -} - -if (!array_key_exists('idpentityid', $_REQUEST)) { - throw new Error\BadRequest('Missing idpentityid to discovery service response handler'); -} - -$state = Auth\State::loadState($_REQUEST['AuthID'], 'saml:sp:sso'); - -// Find authentication source -Assert::keyExists($state, 'saml:sp:AuthId'); -$sourceId = $state['saml:sp:AuthId']; - -$source = Auth\Source::getById($sourceId); -if ($source === null) { - throw new Exception('Could not find authentication source with id ' . $sourceId); -} -if (!($source instanceof \SimpleSAML\Module\saml\Auth\Source\SP)) { - throw new Error\Exception('Source type changed?'); -} - -$source->startSSO($_REQUEST['idpentityid'], $state); diff --git a/modules/saml/www/sp/metadata.php b/modules/saml/www/sp/metadata.php deleted file mode 100644 index 4992710fb..000000000 --- a/modules/saml/www/sp/metadata.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -use SimpleSAML\Auth; -use SimpleSAML\Configuration; -use SimpleSAML\Error; -use SimpleSAML\Metadata; -use SimpleSAML\Module; -use SimpleSAML\Store\StoreFactory; -use SimpleSAML\Utils; - -if (!array_key_exists('PATH_INFO', $_SERVER)) { - throw new Error\BadRequest('Missing authentication source id in metadata URL'); -} - -$config = Configuration::getInstance(); -if ($config->getOptionalBoolean('admin.protectmetadata', false)) { - $authUtils = new Utils\Auth(); - $authUtils->requireAdmin(); -} -$sourceId = substr($_SERVER['PATH_INFO'], 1); -$source = Auth\Source::getById($sourceId); -if ($source === null) { - throw new Error\AuthSource($sourceId, 'Could not find authentication source.'); -} - -if (!($source instanceof Module\saml\Auth\Source\SP)) { - throw new Error\AuthSource( - $sourceId, - 'The authentication source is not a SAML Service Provider.' - ); -} - -$entityId = $source->getEntityId(); -$spconfig = $source->getMetadata(); -$metaArray20 = $source->getHostedMetadata(); - -$storeType = $config->getOptionalString('store.type', 'phpsession'); -$store = StoreFactory::getInstance($storeType); - -$metaBuilder = new Metadata\SAMLBuilder($entityId); -$metaBuilder->addMetadataSP20($metaArray20, $source->getSupportedProtocols()); -$metaBuilder->addOrganizationInfo($metaArray20); - -$xml = $metaBuilder->getEntityDescriptorText(); - -// sign the metadata if enabled -$xml = Metadata\Signer::sign($xml, $spconfig->toArray(), 'SAML 2 SP'); - -header('Content-Type: application/samlmetadata+xml'); -header('Content-Disposition: attachment; filename="' . basename($sourceId) . '.xml"'); -echo($xml); diff --git a/modules/saml/www/sp/saml2-acs.php b/modules/saml/www/sp/saml2-acs.php deleted file mode 100644 index 091a65781..000000000 --- a/modules/saml/www/sp/saml2-acs.php +++ /dev/null @@ -1,280 +0,0 @@ -<?php - -/** - * Assertion consumer service handler for SAML 2.0 SP authentication client. - */ - -use SAML2\Binding; -use SAML2\Assertion; -use SAML2\Exception\Protocol\UnsupportedBindingException; -use SAML2\HTTPArtifact; -use SAML2\Response; -use SimpleSAML\Assert\Assert; -use SimpleSAML\Auth; -use SimpleSAML\Configuration; -use SimpleSAML\Error; -use SimpleSAML\Module; -use SimpleSAML\Logger; -use SimpleSAML\Session; -use SimpleSAML\Store\StoreFactory; -use SimpleSAML\Utils; - -if (!array_key_exists('PATH_INFO', $_SERVER)) { - throw new Error\BadRequest('Missing authentication source ID in assertion consumer service URL'); -} - -$sourceId = substr($_SERVER['PATH_INFO'], 1); - -/** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */ -$source = Auth\Source::getById($sourceId, '\SimpleSAML\Module\saml\Auth\Source\SP'); - -$spMetadata = $source->getMetadata(); -try { - $b = Binding::getCurrentBinding(); -} catch (UnsupportedBindingException $e) { - throw new Error\Error('ACSPARAMS', $e, 400); -} - -if ($b instanceof HTTPArtifact) { - $b->setSPMetadata($spMetadata); -} - -$response = $b->receive(); -if (!($response instanceof Response)) { - throw new Error\BadRequest('Invalid message received at AssertionConsumerService endpoint.'); -} - -$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 Assertion) { - // we found an unencrypted assertion, there should be an issuer here - $issuer = $a->getIssuer(); - break; - } - } - if ($issuer === null) { - // no issuer found in the assertions - throw new Exception('Missing <saml:Issuer> in message delivered to AssertionConsumerService.'); - } -} -$issuer = $issuer->getValue(); - -$session = Session::getSessionFromRequest(); -$prevAuth = $session->getAuthData($sourceId, 'saml:sp:prevAuth'); - -$httpUtils = new Utils\HTTP(); -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. - */ - Logger::info( - 'Duplicate SAML 2 response detected - ignoring the response and redirecting the user to the correct page.' - ); - if (isset($prevAuth['redirect'])) { - $httpUtils->redirectTrustedURL($prevAuth['redirect']); - } - - Logger::info('No RelayState or ReturnURL available, cannot redirect.'); - throw new 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 = Auth\State::loadState($stateId, 'saml:sp:sso'); - } catch (Exception $e) { - // something went wrong, - Logger::warning(sprintf( - 'Could not load state specified by InResponseTo: %s Processing response as unsolicited.', - $e->getMessage(), - )); - } -} - -$enableUnsolicited = $spMetadata->getOptionalBoolean('enable_unsolicited', true); -if ($state === null && $enableUnsolicited === false) { - throw new Error\BadRequest('Unsolicited responses are denied by configuration.'); -} - -if ($state) { - // check that the authentication source is correct - Assert::keyExists($state, 'saml:sp:AuthId'); - if ($state['saml:sp:AuthId'] !== $sourceId) { - throw new 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::keyExists($state, 'ExpectedIssuer'); - if ($state['ExpectedIssuer'] !== $issuer) { - $idpMetadata = $source->getIdPMetadata($issuer); - $idplist = $idpMetadata->getOptionalArrayize('IDPList', []); - if (!in_array($state['ExpectedIssuer'], $idplist, true)) { - 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->getOptionalString('RelayState', $response->getRelayState()); - $state = [ - 'saml:sp:isUnsolicited' => true, - 'saml:sp:AuthId' => $sourceId, - 'saml:sp:RelayState' => $relaystate === null ? null : $httpUtils->checkURLAllowed($relaystate), - ]; -} - -Logger::debug('Received SAML2 Response from ' . var_export($issuer, true) . '.'); - -if (is_null($idpMetadata)) { - $idpMetadata = $source->getIdPmetadata($issuer); -} - -try { - $assertions = Module\saml\Message::processResponse($spMetadata, $idpMetadata, $response); -} catch (Module\saml\Error $e) { - // the status of the response wasn't "success" - $e = $e->toException(); - Auth\State::throwException($state, $e); - return; -} - -$authenticatingAuthority = null; -$nameId = null; -$sessionIndex = null; -$expire = null; -$attributes = []; -$foundAuthnStatement = false; - -$config = Configuration::getInstance(); -$storeType = $config->getOptionalString('store.type', 'phpsession'); - -$store = StoreFactory::getInstance($storeType); - -foreach ($assertions as $assertion) { - // check for duplicate assertion (replay attack) - if ($store !== false) { - $aID = $assertion->getId(); - if ($store->get('saml.AssertionReceived', $aID) !== null) { - $e = new Error\Exception('Received duplicate assertion.'); - 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 Error\Exception('No AuthnStatement found in assertion(s).'); - 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 - 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::true(false); diff --git a/modules/saml/www/sp/saml2-logout.php b/modules/saml/www/sp/saml2-logout.php deleted file mode 100644 index 29478862a..000000000 --- a/modules/saml/www/sp/saml2-logout.php +++ /dev/null @@ -1,160 +0,0 @@ -<?php - -/** - * Logout endpoint handler for SAML SP authentication client. - * - * This endpoint handles both logout requests and logout responses. - */ - -use Exception; -use SAML2\Binding; -use SAML2\Constants; -use SAML2\Exception\Protocol\UnsupportedBindingException; -use SAML2\LogoutResponse; -use SAML2\LogoutRequest; -use SAML2\SOAP; -use SAML2\XML\saml\Issuer; -use SimpleSAML\Auth; -use SimpleSAML\Error; -use SimpleSAML\Logger; -use SimpleSAML\Metadata; -use SimpleSAML\Module; -use SimpleSAML\Utils; - -if (!array_key_exists('PATH_INFO', $_SERVER)) { - throw new Error\BadRequest('Missing authentication source ID in logout URL'); -} - -$sourceId = substr($_SERVER['PATH_INFO'], 1); - -/** @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); -} elseif (!($source instanceof \SimpleSAML\Module\saml\Auth\Source\SP)) { - throw new Error\Exception('Source type changed?'); -} - -try { - $binding = Binding::getCurrentBinding(); -} catch (UnsupportedBindingException $e) { - throw new Error\Error('SLOSERVICEPARAMS', $e, 400); -} -$message = $binding->receive(); - -$issuer = $message->getIssuer(); -if ($issuer instanceof Issuer) { - $idpEntityId = $issuer->getValue(); -} else { - $idpEntityId = $issuer; -} - -if ($idpEntityId === null) { - // Without an issuer we have no way to respond to the message. - throw new Error\BadRequest('Received message on logout endpoint without issuer.'); -} - -$spEntityId = $source->getEntityId(); - -$metadata = Metadata\MetaDataStorageHandler::getMetadataHandler(); -$idpMetadata = $source->getIdPMetadata($idpEntityId); -$spMetadata = $source->getMetadata(); - -Module\saml\Message::validateMessage($idpMetadata, $spMetadata, $message); - -$httpUtils = new Utils\HTTP(); -$destination = $message->getDestination(); -if ($destination !== null && $destination !== $httpUtils->getSelfURLNoQuery()) { - throw new Error\Exception('Destination in logout message is wrong.'); -} - -if ($message instanceof LogoutResponse) { - $relayState = $message->getRelayState(); - if ($relayState === null) { - // Somehow, our RelayState has been lost. - throw new Error\BadRequest('Missing RelayState in logout response.'); - } - - if (!$message->isSuccess()) { - Logger::warning( - 'Unsuccessful logout. Status was: ' . Module\saml\Message::getResponseError($message) - ); - } - - $state = Auth\State::loadState($relayState, 'saml:slosent'); - $state['saml:sp:LogoutStatus'] = $message->getStatus(); - \SimpleSAML\Auth\Source::completeLogout($state); -} elseif ($message instanceof LogoutRequest) { - Logger::debug('module/saml2/sp/logout: Request from ' . $idpEntityId); - Logger::stats('saml20-idp-SLO idpinit ' . $spEntityId . ' ' . $idpEntityId); - - if ($message->isNameIdEncrypted()) { - try { - $keys = Module\saml\Message::getDecryptionKeys($idpMetadata, $spMetadata); - } catch (Exception $e) { - throw new Error\Exception('Error decrypting NameID: ' . $e->getMessage()); - } - - $blacklist = Module\saml\Message::getBlacklistedAlgorithms($idpMetadata, $spMetadata); - - $lastException = null; - foreach ($keys as $i => $key) { - try { - $message->decryptNameId($key, $blacklist); - Logger::debug('Decryption with key #' . $i . ' succeeded.'); - $lastException = null; - break; - } catch (Exception $e) { - 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 = 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 = Module\saml\Message::buildLogoutResponse($spMetadata, $idpMetadata); - $lr->setRelayState($message->getRelayState()); - $lr->setInResponseTo($message->getId()); - - if ($numLoggedOut < count($sessionIndexes)) { - Logger::warning('Logged out of ' . $numLoggedOut . ' of ' . count($sessionIndexes) . ' sessions.'); - } - - $dst = $idpMetadata->getEndpointPrioritizedByBinding( - 'SingleLogoutService', - [ - Constants::BINDING_HTTP_REDIRECT, - Constants::BINDING_HTTP_POST - ] - ); - - if (!($binding instanceof SOAP)) { - $binding = Binding::getBinding($dst['Binding']); - if (isset($dst['ResponseLocation'])) { - $dst = $dst['ResponseLocation']; - } else { - $dst = $dst['Location']; - } - $binding->setDestination($dst); - } else { - $lr->setDestination($dst['Location']); - } - - $binding->send($lr); -} else { - throw new Error\BadRequest('Unknown message received on logout endpoint: ' . get_class($message)); -} diff --git a/modules/saml/www/sp/wrong_authncontextclassref.php b/modules/saml/www/sp/wrong_authncontextclassref.php deleted file mode 100644 index da2ed84f9..000000000 --- a/modules/saml/www/sp/wrong_authncontextclassref.php +++ /dev/null @@ -1,8 +0,0 @@ -<?php - -use SimpleSAML\Configuration; -use SimpleSAML\XHTML\Template; - -$globalConfig = Configuration::getInstance(); -$t = new Template($globalConfig, 'saml:sp/wrong_authncontextclassref.twig'); -$t->send(); diff --git a/phpcs.xml b/phpcs.xml index 5f39d24af..012ba6be0 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -16,10 +16,8 @@ <file>www</file> <exclude-pattern>modules/adfs/*</exclude-pattern> - <exclude-pattern>www/assets/css/stylesheet.css</exclude-pattern> - <exclude-pattern>www/assets/js/bundle.js</exclude-pattern> - <exclude-pattern>www/assets/js/logout.js</exclude-pattern> - <exclude-pattern>www/assets/js/stylesheet.js</exclude-pattern> + <exclude-pattern>www/assets/css/*</exclude-pattern> + <exclude-pattern>www/assets/js/*</exclude-pattern> <!-- This is the rule we inherit from. If you want to exlude some specific rules, see the docs on how to do that --> <rule ref="PSR12"/> @@ -34,6 +32,7 @@ <!-- Exclude files with long lines that we cannot immediately fix --> <exclude-pattern>tests/lib/SimpleSAML/Metadata/MetaDataStorageSourceTest.php</exclude-pattern> <exclude-pattern>tests/lib/SimpleSAML/Metadata/SAMLParserTest.php</exclude-pattern> + <exclude-pattern>tests/modules/saml/lib/Controller/ServiceProviderTest.php</exclude-pattern> </rule> <!-- Ignore files with side effects that we cannot fix --> diff --git a/psalm.xml b/psalm.xml index 2d2bee768..c03a78417 100644 --- a/psalm.xml +++ b/psalm.xml @@ -63,7 +63,6 @@ <MissingFile> <errorLevel type="suppress"> <file name="www/*.php" /> - <file name="modules/*/www/*.php" /> </errorLevel> </MissingFile> diff --git a/tests/modules/saml/lib/Auth/Source/SPTest.php b/tests/modules/saml/lib/Auth/Source/SPTest.php index 0387ae082..53559002e 100644 --- a/tests/modules/saml/lib/Auth/Source/SPTest.php +++ b/tests/modules/saml/lib/Auth/Source/SPTest.php @@ -459,7 +459,7 @@ class SPTest extends ClearStateTestCase $this->assertIsArray($md['AssertionConsumerService']); foreach ($md['AssertionConsumerService'] as $acs) { $this->assertEquals( - 'http://localhost/simplesaml/module.php/saml/sp/saml2-acs.php/' . $spId, + 'http://localhost/simplesaml/module.php/saml/sp/assertionConsumerService/' . $spId, $acs['Location'] ); $this->assertStringStartsWith('urn:oasis:names:tc:SAML:2.0:bindings', $acs['Binding']); @@ -1198,7 +1198,10 @@ class SPTest extends ClearStateTestCase $hok = $md['AssertionConsumerService'][2]; $this->assertIsArray($hok); $this->assertEquals('urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', $hok['Binding']); - $this->assertEquals('http://localhost/simplesaml/module.php/saml/sp/saml2-acs.php/' . $spId, $hok['Location']); + $this->assertEquals( + 'http://localhost/simplesaml/module.php/saml/sp/assertionConsumerService/' . $spId, + $hok['Location'] + ); $this->assertEquals(2, $hok['index']); $this->assertEquals('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', $hok['hoksso:ProtocolBinding']); } diff --git a/tests/modules/saml/lib/Controller/DiscoTest.php b/tests/modules/saml/lib/Controller/DiscoTest.php new file mode 100644 index 000000000..a5311d3f8 --- /dev/null +++ b/tests/modules/saml/lib/Controller/DiscoTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\saml\Controller; + +use PHPUnit\Framework\TestCase; +use SimpleSAML\Configuration; +use SimpleSAML\Http\RunnableResponse; +use SimpleSAML\Module\saml\Controller; +use Symfony\Component\HttpFoundation\Request; + +/** + * Set of tests for the controllers in the "saml" module. + * + * @covers \SimpleSAML\Module\saml\Controller\Disco + * @package SimpleSAML\Test + */ +class DiscoTest extends TestCase +{ + /** @var \SimpleSAML\Configuration */ + protected Configuration $config; + + + /** + * Set up for each test. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = Configuration::loadFromArray( + [ + 'module.enable' => ['saml' => true], + ], + '[ARRAY]', + 'simplesaml' + ); + Configuration::setPreLoadedConfig($this->config, 'config.php'); + } + + + /** + * Test that accessing the disco-endpoint leads to a RunnableResponse + * + * @return void + */ + public function testDisco(): void + { + $params = [ + 'entityID' => 'urn:entity:phpunit', + 'return' => '/something', + 'isPassive' => 'true', + 'IdPentityID' => 'some:idp:phpunit', + 'returnIDParam' => 'someParam', + 'IDPList' => 'a,b,c', + ]; + + $_GET = array_merge($_GET, $params); + $_SERVER['REQUEST_URI'] = '/disco'; + + $c = new Controller\Disco($this->config); + + $result = $c->disco(); + $this->assertInstanceOf(RunnableResponse::class, $result); + } +} diff --git a/tests/modules/saml/lib/Controller/ProxyTest.php b/tests/modules/saml/lib/Controller/ProxyTest.php new file mode 100644 index 000000000..7ebfdf4ad --- /dev/null +++ b/tests/modules/saml/lib/Controller/ProxyTest.php @@ -0,0 +1,165 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\saml\Controller; + +use PHPUnit\Framework\TestCase; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\Http\RunnableResponse; +use SimpleSAML\Module\saml\Controller; +use SimpleSAML\Module\saml\Error\NoAvailableIDP; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\Request; + +/** + * Set of tests for the controllers in the "saml" module. + * + * @covers \SimpleSAML\Module\saml\Controller\Proxy + * @package SimpleSAML\Test + */ +class ProxyTest extends TestCase +{ + /** @var \SimpleSAML\Configuration */ + protected Configuration $config; + + + /** + * Set up for each test. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = Configuration::loadFromArray( + [ + 'module.enable' => ['saml' => true], + ], + '[ARRAY]', + 'simplesaml' + ); + Configuration::setPreLoadedConfig($this->config, 'config.php'); + + Configuration::setPreLoadedConfig( + Configuration::loadFromArray( + [ + 'phpunit' => ['saml:SP'], + ], + '[ARRAY]', + 'simplesaml' + ), + 'authsources.php', + 'simplesaml' + ); + } + + + /** + * Test that accessing the invalidSession-endpoint without StateId leads to an exception + * + * @return void + */ + public function testMissingStateId(): void + { + $request = Request::create( + '/invalidSesssion', + 'POST', + ); + + $c = new Controller\Proxy($this->config); + + $this->expectException(Error\BadRequest::class); + $this->expectExceptionMessage('Missing mandatory parameter: AuthState'); + + $c->invalidSession($request); + } + + + /** + * Test that accessing the invalidSession-endpoint with StateId results in a Template + * + * @return void + */ + public function testWithStateId(): void + { + $request = Request::create( + '/invalidSesssion?AuthState=someState', + 'POST', + ); + + $c = new Controller\Proxy($this->config); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return [ + 'saml:sp:IdPMetadata' => Configuration::loadFromArray(['test' => 'phpunit']), + 'SPMetadata' => 'something else', + ]; + } + }); + + $result = $c->invalidSession($request); + $this->assertInstanceOf(Template::class, $result); + } + + + /** + * Test that accessing the invalidSession-endpoint with StateId and + * with pressing cancel results in a RunnableResponse + * + * @return void + */ + public function testWithStateIdCancel(): void + { + $request = Request::create( + '/invalidSesssion?AuthState=someState', + 'POST', + ['cancel' => 'cancel'], + ); + + $c = new Controller\Proxy($this->config); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return [ + 'saml:sp:IdPMetadata' => Configuration::loadFromArray(['test' => 'phpunit']), + 'SPMetadata' => 'something else', + ]; + } + }); + + $this->expectException(NoAvailableIDP::class); + $c->invalidSession($request); + } + + + /** + * Test that accessing the invalidSession-endpoint with StateId and + * with pressing continue results in a RunnableResponse + * + * @return void + */ + public function testWithStateIdContinue(): void + { + $request = Request::create( + '/invalidSesssion?AuthState=someState', + 'POST', + ['continue' => 'continue'], + ); + + $c = new Controller\Proxy($this->config); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return [ + 'saml:sp:AuthId' => 'phpunit', + ]; + } + }); + + $result = $c->invalidSession($request); + $this->assertInstanceOf(RunnableResponse::class, $result); + } +} diff --git a/tests/modules/saml/lib/Controller/ServiceProviderTest.php b/tests/modules/saml/lib/Controller/ServiceProviderTest.php new file mode 100644 index 000000000..dd9b130f3 --- /dev/null +++ b/tests/modules/saml/lib/Controller/ServiceProviderTest.php @@ -0,0 +1,438 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\saml\Controller; + +use Exception; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\Http\RunnableResponse; +use SimpleSAML\Module\saml\Controller; +use SimpleSAML\Session; +use SimpleSAML\Utils; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Set of tests for the controllers in the "saml" module. + * + * @covers \SimpleSAML\Module\saml\Controller\ServiceProvider + * @package SimpleSAML\Test + */ +class ServiceProviderTest extends TestCase +{ + /** @var \SimpleSAML\Configuration */ + protected Configuration $config; + + /** @var \SimpleSAML\Session */ + protected Session $session; + + /** @var \SimpleSAML\Utils\Auth */ + protected Utils\Auth $authUtils; + + + /** + * Set up for each test. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->session = Session::getSessionFromRequest(); + $this->config = Configuration::loadFromArray( + [ + 'module.enable' => ['saml' => true], + 'admin.protectmetadata' => true, + ], + '[ARRAY]', + 'simplesaml' + ); + Configuration::setPreLoadedConfig($this->config, 'config.php'); + + Configuration::setPreLoadedConfig( + Configuration::loadFromArray( + [ + 'admin' => ['core:AdminPassword'], + 'phpunit' => ['saml:SP'], + ], + '[ARRAY]', + 'simplesaml' + ), + 'authsources.php', + 'simplesaml' + ); + + $this->authUtils = new class () extends Utils\Auth { + public function requireAdmin(): void + { + // stub + } + }; + } + + + /** + * Test that accessing the discoResponse-endpoint without AuthID leads to an exception + * + * @return void + */ + public function testDiscoResponseMissingAuthId(): void + { + $request = Request::create( + '/discoResponse', + 'GET', + ); + + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\BadRequest::class); + $this->expectExceptionMessage('Missing AuthID to discovery service response handler'); + + $c->discoResponse($request); + } + + + /** + * Test that accessing the discoResponse-endpoint with AuthID but without idpentityid results in an exception + * + * @return void + */ + public function testWithAuthIdWithoutEntity(): void + { + $request = Request::create( + '/discoResponse', + 'GET', + ['AuthID' => 'abc123'] + ); + + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\BadRequest::class); + $this->expectExceptionMessage('Missing idpentityid to discovery service response handler'); + + $c->discoResponse($request); + } + + + /** + * Test that accessing the discoResponse-endpoint with unknown authsource in state results in an exception + * + * @return void + */ + public function testWithUnknownAuthSource(): void + { + $request = Request::create( + '/discoResponse', + 'GET', + ['AuthID' => 'abc123', 'idpentityid' => 'urn:idp:entity'], + ); + + $c = new Controller\ServiceProvider($this->config, $this->session); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return [ + 'saml:sp:AuthId' => 'unknown', + ]; + } + }); + + $this->expectException(Exception::class); + $c->discoResponse($request); + } + + + /** + * Test that accessing the discoResponse-endpoint with non-SP authsource in state results in an exception + * + * @return void + */ + public function testWithNonSPAuthSource(): void + { + $request = Request::create( + '/discoResponse', + 'GET', + ['AuthID' => 'abc123', 'idpentityid' => 'urn:idp:entity'], + ); + + $c = new Controller\ServiceProvider($this->config, $this->session); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return [ + 'saml:sp:AuthId' => 'admin', + ]; + } + }); + + $this->expectException(Error\Exception::class); + $c->discoResponse($request); + } + + + /** + * Test that accessing the discoResponse-endpoint with SP authsource in state results in a RunnableResponse + * + * @return void + */ + public function testWithSPAuthSource(): void + { + $request = Request::create( + '/discoResponse', + 'GET', + ['AuthID' => 'abc123', 'idpentityid' => 'urn:idp:entity'], + ); + + $c = new Controller\ServiceProvider($this->config, $this->session); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return [ + 'saml:sp:AuthId' => 'phpunit', + ]; + } + }); + + $result = $c->discoResponse($request); + $this->assertInstanceOf(RunnableResponse::class, $result); + } + + + /** + * Test that accessing the wrongAuthnContextClassRef-endpoint without AuthID leads to a Template + * + * @return void + */ + public function testWrongAuthnContextClassRef(): void + { + $c = new Controller\ServiceProvider($this->config, $this->session); + + $result = $c->wrongAuthnContextClassRef(); + $this->assertInstanceOf(Template::class, $result); + } + + + /** + * Test that accessing the ACS-endpoint with unknown SourceID results in an exception + * + * @return void + */ + public function testACSWithUnknownSourceID(): void + { + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\Exception::class); + $this->expectExceptionMessage("No authentication source with id 'something' found."); + + $c->assertionConsumerService('something'); + } + + + /** + * Test that accessing the ACS-endpoint without being able to determine the binding results in an exception + * + * @return void + */ + public function testACSWithUnkownBinding(): void + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\Error::class); + $this->expectExceptionMessage('ACSPARAMS'); + + $c->assertionConsumerService('phpunit'); + } + + + /** + * Test that accessing the ACS-endpoint with a request instead of a response results in an exception + * + * @return void + */ + public function testACSWithWrongMessage(): void + { + $q = [ + 'SAMLRequest' => 'pVJNb9swDP0rhu6O7XjeGiEJkDYoGqDbgibboZdCkahEgEx5Ir11/36y02FdD7n0JPDjPT4+cU6q9Z1c9XzCB/jRA3H23HokORYWoo8ogyJHElULJFnL3erzvZxOStnFwEEHL15BLiMUEUR2AUW2WS/EUw2NrXRp7NWshEPVzJqm+TQzVV1DddC21rUy1tq6norsO0RKyIVIRAlO1MMGiRVySpVVk1fTvKr25ZVsGvnh46PI1mkbh4pH1Im5I1kUgEeHMKE+Wh0QnnmCvlBpf0B2emwunOkKcnj0kJM7Yj7oXf2VfhOQ+hbiDuJPp+Hbw/0/8uSIdf4tO7m28zC4U7TB9KnendKAIabzO82VpjFrwKrec06dyLYv/l47NEnNZWsP5yaSd/v9Nt9+3e3Fcj5wy9GquHyPxhZYGcXqjcR58XrA/HxLX5K0zXobvNO/s9sQW8WXlQ8ZZ3I7tkqOCsmlz0iWex9+3URQDAvBsQdRLM8j/7/Y5R8=', + 'RelayState' => 'https://profile.surfconext.nl/', + 'SAMLEncoding' => 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE', + ]; + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['QUERY_STRING'] = http_build_query($q); + $_GET['SAMLRequest'] = $q['SAMLRequest']; + $_GET['RelayState'] = $q['RelayState']; + $_GET['SAMLEncoding'] = $q['SAMLEncoding']; + + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\BadRequest::class); + $this->expectExceptionMessage('Invalid message received at AssertionConsumerService endpoint.'); + + $c->assertionConsumerService('phpunit'); + } + + + /** + * Test that accessing the ACS-endpoint with a response from an unknown entity leads to an exception + * + * @return void + */ + public function testACSWithCorrectMessageUnknownEntity(): void + { + $q = [ + 'SAMLResponse' => 'vVdbc6rIGn2fX2G5H1MJd0RrJ1UgKmDQIIiXl1NANzcRCA2i/vppNLpNJsnMnqo5T9qL/q7r62bxEznbJO/NIMqzFMHWfpukqHcCH9tVkfYyB0WolzpbiHql1zNF/blHP5C9vMjKzMuS9o3J9xYOQrAooyxtt1T5sd2fzqxplxVIhoIMCbkuIEnehSRJ0xRJdroU1fFc6HWA4Hiw3bJhgbDtYxu7wg4QqqCaotJJSwyRFHdP0fcUb1Fsj2V7pLBut2SIyih1ypNVWJY56hFEGW6iexSBh7y8Z4WHqiweUFX4XpJV4CFNiC1Mkiwl8gyVl57gaOnlv5U9tv9HdzsSTQ4GlCB0hqQ8xD84XYHmOzxFMkOh/fSz6UbvlGTxdAkN0yBK4UOJ0zrHzFK4L5ugTlWGMC0j75QsEYEc51E6wCmdn8Stq59ntszSKSv0ftXPAGzZTlLB71lAp909s/I8iFCbeDpHeO+0J164emN3j6JzD3EddV0/1MxDVgQETZIUsdSfTS+EW+c+OhHSsHWx+nuj22HoMgwA0KV53GHKFQTHcR2PZT0SQEHgfAggECB0/8Uw/HeMANzLKMBjVhWX0wO+KpskyC6B9wAUBT/aT3+0WhdzCNTUz07e+k6apThwEh1PwXVYhhloiUmQFVEZbr9sKUU2vu/h3rv3KDb9gbnFEX7FOKX4D729y7RAzj0KHerssHE3gz4sIGa6NZ+pj+0fv0ffqUyrcFLkZ8UWvV/+Xmow3cEkyyHAZ/qtwmakf8vhp537Sfw1RzkK8KT8mw6+de+Xk9NBfdou75YRhJU/UXO3XN7ZQjh2p/mwFsXHUwK3m0/AtfHn5YfRubJ8tuhXCeETSyl2wWg+X5aA3K4SL6ZhcjciFlLVCUcCKUOKMRcSuwfiRCIPSyXNd5bvktb+TkUvuwUi2CWnOgdt42XqgZ75x2dIB0p3bB61VyllUn4Z18mEu6P8TjGtzLkrBfOIOGp70Y6D9apEu1gJkklHFgVlM1TvFra/P2SvSubm0kE6slSaB/PHazk3+f/R1DSGh2t9S47syvgIXhf95pLym1MKn3RV7d8d+31xawZirUpioGrii7Z7jg00m7GRLpKjvvk6MlWXkY2BJBlzUR+S+/5R1KRgYkviyhI3nK7PxFoOVrJtGOqgBjZQtGRFh+QNrnyBjzFu2bY2crc2xoN6eMblQd0l18sJqUfScbWgkDqaJF5q1EroTXRrXutHldQtA/8OmEWDxSdsf8ViCegGqvvGyd9oUGtTSx4YusiORGo+6Eu6Yi9nh/VikoEbXNp/jvdDXZlTtjnbcgnGV7q0OuHiXn8BI/sIZDXw6GHp9qV4vdRIXR35H/sn4v5hdxNR7kuRMZYCo78fr4p0IUzqVzCrZ7WjVJjGZ74bGENLc4GfieZzuIog7U6jTDtoNeXfeeIuj1HCmvYq3w0QVGG5VITnIN90xh3mZSNrzwIYBiwaxMMhHwfbuJgOTCbbE0FpgS4g7PpFUYndi+qNDhRybY3NB/ow4YAwmorHTNtMFuAl7tY2W+zYasdwunMQalUWDVHKWKWPayN0iWzqB3JgLCTJslTnpc7HOSZYgKpuHiez9Tw2SzeKcUNqzMGMjCV1pGDbQfDd/tdhmA+FemkNnnVxc+Yk1PvWpt4PZHF6nrvAkiib9LZ27CjGDe59gWcYn9jzzbpaL439SBYXZ1y3ZGaWeIxxUJVJ6C7qYEXbB6B+PAf1qdZBbQx1UZdEX6hlY6WNs7Ua7ryJaAyGkiHikR6IY2Gws+bkc6BoyKzwhTeF22CW5LnuayKcbqtqPQlNfUUbYbUdTte5I7rCZKjW8/HcPhy01Mw6G35TKv2x2mWRgbodnmbp0JLl1aBeaHJXCUUkvk4zmpo7QrC2GIGotzwNmXFQjJe7NIlFd/yylJeazjqbY+fAK3y926kji/dJn4y0hfLKsHFdn2+Qj5fCFTxfG8TthfLuxnkTCGblxtAr31YTrJ5UuTXEbwCn/F5WNUgE7v3T1l7ZvDgiLCDaT6TDsb7LdEm+i2Uiw3DAYSDLkKxP+RzPOMDlXZ73+TdZcQ75Ppt+lvpR47fRY+fXz/fJeNueC50CFu2vHTUNaU2ycppOC9EvYfEX5dQ9y+gZ9KK8qeX/LKIvyvSz5D88eqsS7wBR8xg1hUkQkwE/04MdXNU/qPwihSsQNW9cnH1ZRN45/LsnT7/Zlw9K8urmw/pdQOJDhdcUyjBtlDvcYoZap+VXSpjucRyu3MSyH3v4sgE03WOZHi382qqmAO4xZZwLuC5Pczzrk5zHeRTPAMahXExcx6V8yDGMA7sQeKB9mx5OusSy+hOon+BvQixpnr79bPR6XrMPwy/4p84KcG3UJ65+Rbno9zRoVo1YO1yZwpIxxaL+wceHFj6k2Y3Hz8w+CfgOuzJwRS/fT9fPq8vwP/0J', + ]; + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['QUERY_STRING'] = http_build_query($q); + $_GET['SAMLResponse'] = $q['SAMLResponse']; + + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\MetadataNotFound::class); +// Breaks PHP 8.1 tests +// $this->expectExceptionMessage("METADATANOTFOUND('%ENTITYID%' => '\'https://engine.test.surfconext.nl/authentication/idp/metadata\'')"); + + $c->assertionConsumerService('phpunit'); + } + + + /** + * Test that accessing the SLO-endpoint with unknown SourceID results in an exception + * + * @return void + */ + public function testSLOWithUnknownSourceID(): void + { + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\Exception::class); + $this->expectExceptionMessage("No authentication source with id 'something' found."); + + $c->singleLogoutService('something'); + } + + + /** + * Test that accessing the SLO-endpoint without being able to determine the binding results in an exception + * + * @return void + */ + public function testSLOWithUnkownBinding(): void + { + $_SERVER['REQUEST_METHOD'] = 'PUT'; + $_SERVER['QUERY_STRING'] = ''; + unset($_GET['SAMLResponse']); + + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\Error::class); + $this->expectExceptionMessage('SLOSERVICEPARAMS'); + + $c->singleLogoutService('phpunit'); + } + + + /** + * Test that accessing the SLO-endpoint with a response from an unknown entity leads to an exception + * + * @return void + */ + public function testSLOWithCorrectMessageUnknownEntity(): void + { + /** Note: should be replaced by loading an xml-file once we can load that from saml2v5 */ + $x = <<<XML +<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="_f987e7436c1103bcf89296303f780d853d7713a8ef" IssueInstant="2020-08-15T15:53:24Z"> + <saml:Issuer>TheIssuer</saml:Issuer> + <saml:EncryptedID> + <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element"> + <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/> + <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> + <xenc:EncryptedKey> + <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/> + <xenc:CipherData> + <xenc:CipherValue>QMYbOZfUgJmvzmTeSDqvA8MKzt4M2K9kb0BQtIqMgyn+OlBmRVvebZEkW/5k5E9qPtMUBCPiTatJ8aNb7Z4DuPqTRODgGzR7LEyOxe8JTPbzn/xwkVHRwzMkodDUkTDDIkJr7Tzyseg1crGZf21q8pBJfGXSaPamIC2ncjuOcs0=</xenc:CipherValue> + </xenc:CipherData> + </xenc:EncryptedKey> + </ds:KeyInfo> + <xenc:CipherData> + <xenc:CipherValue>U0czb6qGEvLTQaVcJDNjuAJvMAV4CcRQYvxUoBgfCP1T1xh14Kez8k0VHyEtgf6TYG7dK87SBzZccsn7MlLDLFbWH807bJ+cnXSQ9+8dXB/aohfXnmNSZUcIQiPLr5oNq1g6GYhgiNShDnYZUG3ffQ==</xenc:CipherValue> + </xenc:CipherData> + </xenc:EncryptedData> + </saml:EncryptedID> + <samlp:SessionIndex>SomeSessionIndex1</samlp:SessionIndex> + <samlp:SessionIndex>SomeSessionIndex2</samlp:SessionIndex> +</samlp:LogoutRequest> +XML; + $q = [ + 'SAMLRequest' => base64_encode(gzdeflate($x)), + 'RelayState' => 'https://profile.surfconext.nl/', + 'SAMLEncoding' => 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE', + ]; + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['QUERY_STRING'] = http_build_query($q); + $_GET['SAMLRequest'] = $q['SAMLRequest']; + $_GET['RelayState'] = $q['RelayState']; + $_GET['SAMLEncoding'] = $q['SAMLEncoding']; + + $c = new Controller\ServiceProvider($this->config, $this->session); + + $this->expectException(Error\MetadataNotFound::class); +// Breaks PHP 8.1 tests +// $this->expectExceptionMessage("METADATANOTFOUND('%ENTITYID%' => '\'https://engine.test.surfconext.nl/authentication/idp/metadata\'')"); + + $c->singleLogoutService('phpunit'); + } + + + /** + * Test that accessing the metadata-endpoint with or without authentication + * and admin.protectmetadata set to true or false is handled properly + * + * @dataProvider provideMetadataAccess + * @param bool $protected + * @param bool $authenticated + * @return void + */ + public function testMetadataAccess(bool $authenticated, bool $protected): void + { + $c = new Controller\ServiceProvider($this->config, $this->session); + + if ($authenticated === true || $protected === false) { + // Bypass authentication - mock being authenticated + $c->setAuthUtils($this->authUtils); + } + + $result = $c->metadata('phpunit'); + + if ($authenticated !== false && $protected !== true) { + // ($authenticated === true) or ($protected === false) + // Should lead to a Response + $this->assertInstanceOf(Response::class, $result); + } else { + $this->assertInstanceOf(RunnableResponse::class, $result); + } + } + + + /** + * @return array + */ + public function provideMetadataAccess(): array + { + return [ + /* [authenticated, protected] */ + [false, false], + [false, true], + [true, false], + [true, true], + ]; + } +} -- GitLab