diff --git a/composer.json b/composer.json index 6888b1732672c1f32a064b925965a87a0d54d314..14deee4f7f938e55adbc2e9be55a4aad473a20a9 100644 --- a/composer.json +++ b/composer.json @@ -41,10 +41,15 @@ "whitehat101/apr1-md5": "~1.0", "twig/twig": "~1.0 || ~2.0", "gettext/gettext": "^4.6", - "jaimeperez/twig-configurable-i18n": "^2.0" + "jaimeperez/twig-configurable-i18n": "^2.0", + "symfony/routing": "^3.4", + "symfony/http-foundation": "^3.4", + "symfony/config": "^3.4", + "symfony/http-kernel": "^3.4", + "symfony/dependency-injection": "^3.4" }, "require-dev": { - "phpunit/phpunit": "~4.8.35", + "phpunit/phpunit": "~4.8", "mikey179/vfsStream": "~1.6" }, "suggests": { diff --git a/composer.lock b/composer.lock index 33134f8ea9286fc6f6985e1cfdd40dd5c5663c89..4226822899ba13e2a46bb8abd248fd2aeec2cd0a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a5ddd14915964c85316344e4f14c528e", + "content-hash": "9bb30d08fad0383d1455906ecf328ef3", "packages": [ { "name": "gettext/gettext", @@ -173,6 +173,100 @@ ], "time": "2018-10-10T09:12:46+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.99", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "time": "2018-07-02T15:55:56+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, { "name": "psr/log", "version": "1.0.2", @@ -315,9 +409,456 @@ "description": "SAML2 PHP library from SimpleSAMLphp", "time": "2018-11-20T11:11:28+00:00" }, + { + "name": "symfony/config", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "e5389132dc6320682de3643091121c048ff796b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/e5389132dc6320682de3643091121c048ff796b3", + "reference": "e5389132dc6320682de3643091121c048ff796b3", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/filesystem": "~2.8|~3.0|~4.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3", + "symfony/finder": "<3.3" + }, + "require-dev": { + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/event-dispatcher": "~3.3|~4.0", + "symfony/finder": "~3.3|~4.0", + "symfony/yaml": "~3.0|~4.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2018-09-08T13:15:14+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "0a612e9dfbd2ccce03eb174365f31ecdca930ff6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/0a612e9dfbd2ccce03eb174365f31ecdca930ff6", + "reference": "0a612e9dfbd2ccce03eb174365f31ecdca930ff6", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T16:33:53+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "aea20fef4e92396928b5db175788b90234c0270d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/aea20fef4e92396928b5db175788b90234c0270d", + "reference": "aea20fef4e92396928b5db175788b90234c0270d", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/container": "^1.0" + }, + "conflict": { + "symfony/config": "<3.3.7", + "symfony/finder": "<3.3", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "psr/container-implementation": "1.0" + }, + "require-dev": { + "symfony/config": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T12:28:39+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", + "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T09:06:28+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d69930fc337d767607267d57c20a7403d0a822a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d69930fc337d767607267d57c20a7403d0a822a4", + "reference": "d69930fc337d767607267d57c20a7403d0a822a4", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2018-10-02T12:28:39+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3a4498236ade473c52b92d509303e5fd1b211ab1", + "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php70": "~1.6" + }, + "require-dev": { + "symfony/expression-language": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2018-10-03T08:48:18+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "a0944a9a1d8845da724236cde9a310964acadb1c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/a0944a9a1d8845da724236cde9a310964acadb1c", + "reference": "a0944a9a1d8845da724236cde9a310964acadb1c", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/http-foundation": "~3.4.12|~4.0.12|^4.1.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.4.10|<4.0.10,>=4", + "symfony/var-dumper": "<3.3", + "twig/twig": "<1.34|<2.4,>=2" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/cache": "~1.0", + "symfony/browser-kit": "~2.8|~3.0|~4.0", + "symfony/class-loader": "~2.8|~3.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/console": "~2.8|~3.0|~4.0", + "symfony/css-selector": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "^3.4.10|^4.0.10", + "symfony/dom-crawler": "~2.8|~3.0|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/process": "~2.8|~3.0|~4.0", + "symfony/routing": "~3.4|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0", + "symfony/templating": "~2.8|~3.0|~4.0", + "symfony/translation": "~2.8|~3.0|~4.0", + "symfony/var-dumper": "~3.3|~4.0" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "", + "symfony/finder": "", + "symfony/var-dumper": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpKernel Component", + "homepage": "https://symfony.com", + "time": "2018-10-03T12:03:34+00:00" + }, { "name": "symfony/polyfill-ctype", - "version": "v1.10.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -375,16 +916,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.10.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", "shasum": "" }, "require": { @@ -430,7 +971,143 @@ "portable", "shim" ], - "time": "2018-09-21T13:07:52+00:00" + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/1e24b0c4a56d55aaf368763a06c6d1c7d3194934", + "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0|~9.99", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/routing", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "585f6e2d740393d546978769dd56e496a6233e0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/585f6e2d740393d546978769dd56e496a6233e0b", + "reference": "585f6e2d740393d546978769dd56e496a6233e0b", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/config": "<3.3.1", + "symfony/dependency-injection": "<3.3", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "psr/log": "~1.0", + "symfony/config": "^3.3.1|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/http-foundation": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/dependency-injection": "For loading routes from a service", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Routing Component", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "time": "2018-10-02T12:28:39+00:00" }, { "name": "twig/extensions", diff --git a/config-templates/config.php b/config-templates/config.php index 1fa9d3b40f8d1dfd764b319413a0266452135336..09117a875692dbf102ced407c8b185d262346c51 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -727,17 +727,6 @@ $config = [ 'language.cookie.httponly' => false, 'language.cookie.lifetime' => (60 * 60 * 24 * 900), - /* - * Which i18n backend to use. - * - * "SimpleSAMLphp" is the home made system, valid for 1.x. - * For 2.x, only "gettext/gettext" will be possible. - * - * Home-made templates will always use "SimpleSAMLphp". - * To use twig (where avaliable), select "gettext/gettext". - */ - 'language.i18n.backend' => 'SimpleSAMLphp', - /** * Custom getLanguage function called from SimpleSAML\Locale\Language::getLanguage(). * Function should return language code of one of the available languages or NULL. diff --git a/lib/SimpleSAML/Auth/AuthenticationFactory.php b/lib/SimpleSAML/Auth/AuthenticationFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..7335bc2d5643ba76e6d76c5a524ce9af444fe53c --- /dev/null +++ b/lib/SimpleSAML/Auth/AuthenticationFactory.php @@ -0,0 +1,37 @@ +<?php + +namespace SimpleSAML\Auth; + +/** + * Factory class to get instances of \SimpleSAML\Auth\Simple for a given authentication source. + */ +class AuthenticationFactory +{ + + /** @var \SimpleSAML\Configuration */ + protected $config; + + /** @var \SimpleSAML\Session */ + protected $session; + + + public function __construct(\SimpleSAML\Configuration $config, \SimpleSAML\Session $session) + { + $this->config = $config; + $this->session = $session; + } + + + /** + * Create a new instance of \SimpleSAML\Auth\Simple for the given authentication source. + * + * @param string $as The identifier of the authentication source, as indexed in the authsources.php configuration + * file. + * + * @return \SimpleSAML\Auth\Simple + */ + public function create($as) + { + return new Simple($as, $this->config, $this->session); + } +} \ No newline at end of file diff --git a/lib/SimpleSAML/Auth/Simple.php b/lib/SimpleSAML/Auth/Simple.php index a0a0aae44a41eb9771b95bb41b85d49c8f3b89a7..ffaa0cc92a2921fcd53cc9f472cde8fba3a64dbf 100644 --- a/lib/SimpleSAML/Auth/Simple.php +++ b/lib/SimpleSAML/Auth/Simple.php @@ -23,22 +23,34 @@ class Simple */ protected $authSource; - /** - * @var Configuration|null - */ + /** @var \SimpleSAML\Configuration */ protected $app_config; + /** @var \SimpleSAML\Session */ + protected $session; + + /** * Create an instance with the specified authsource. * * @param string $authSource The id of the authentication source. + * @param \SimpleSAML\Configuration|null $config Optional configuration to use. + * @param \SimpleSAML\Session|null $session Optional session to use. */ - public function __construct($authSource) + public function __construct($authSource, Configuration $config = null, Session $session = null) { assert(is_string($authSource)); + if ($config === null) { + $config = Configuration::getInstance(); + } $this->authSource = $authSource; - $this->app_config = Configuration::getInstance()->getConfigItem('application', null); + $this->app_config = $config->getConfigItem('application', null); + + if ($session === null) { + $session = Session::getSessionFromRequest(); + } + $this->session = $session; } @@ -69,9 +81,7 @@ class Simple */ public function isAuthenticated() { - $session = Session::getSessionFromRequest(); - - return $session->isValid($this->authSource); + return $this->session->isValid($this->authSource); } @@ -90,9 +100,7 @@ class Simple */ public function requireAuth(array $params = []) { - $session = Session::getSessionFromRequest(); - - if ($session->isValid($this->authSource)) { + if ($this->session->isValid($this->authSource)) { // Already authenticated return; } @@ -195,14 +203,13 @@ class Simple assert(isset($params['ReturnStateParam'], $params['ReturnStateStage'])); } - $session = Session::getSessionFromRequest(); - if ($session->isValid($this->authSource)) { - $state = $session->getAuthData($this->authSource, 'LogoutState'); + if ($this->session->isValid($this->authSource)) { + $state = $this->session->getAuthData($this->authSource, 'LogoutState'); if ($state !== null) { $params = array_merge($state, $params); } - $session->doLogout($this->authSource); + $this->session->doLogout($this->authSource); $params['LogoutCompletedHandler'] = [get_class(), 'logoutCompleted']; @@ -259,8 +266,7 @@ class Simple } // Authenticated - $session = Session::getSessionFromRequest(); - return $session->getAuthData($this->authSource, 'Attributes'); + return $this->session->getAuthData($this->authSource, 'Attributes'); } @@ -279,8 +285,7 @@ class Simple return null; } - $session = Session::getSessionFromRequest(); - return $session->getAuthData($this->authSource, $name); + return $this->session->getAuthData($this->authSource, $name); } @@ -295,8 +300,7 @@ class Simple return null; } - $session = Session::getSessionFromRequest(); - return $session->getAuthState($this->authSource); + return $this->session->getAuthState($this->authSource); } diff --git a/lib/SimpleSAML/HTTP/Router.php b/lib/SimpleSAML/HTTP/Router.php new file mode 100644 index 0000000000000000000000000000000000000000..e537092a9e62352497e49233f610a9e4a3a93a13 --- /dev/null +++ b/lib/SimpleSAML/HTTP/Router.php @@ -0,0 +1,127 @@ +<?php + +namespace SimpleSAML\HTTP; + +use SimpleSAML\Configuration; +use SimpleSAML\Session; + +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\HttpKernel; +use Symfony\Component\Routing\RequestContext; + +/** + * Class that routes requests to responses. + * + * @package SimpleSAML + */ +class Router +{ + + protected $arguments; + + /** @var \SimpleSAML\Configuration */ + protected $config; + + /** @var RequestContext */ + protected $context; + + /** @var EventDispatcher */ + protected $dispatcher; + + /** @var Request */ + protected $request; + + /** @var \SimpleSAML\Module\ControllerResolver */ + protected $resolver; + + /** @var \SimpleSAML\Session */ + protected $session; + + /** @var RequestStack */ + protected $stack; + + + /** + * Router constructor. + * + * @param string $module + */ + public function __construct($module) + { + $this->arguments = new ArgumentResolver(); + $this->context = new RequestContext(); + $this->resolver = new \SimpleSAML\Module\ControllerResolver($module); + $this->dispatcher = new EventDispatcher(); + } + + + /** + * Process a given request. + * + * If no specific arguments are given, the default instances will be used (configuration, session, etc). + * + * @param Request|null $request The request to process. Defaults to the current one. + * + * @return Response A response suitable for the given request. + * + * @throws \Exception If an error occurs. + */ + public function process(Request $request = null) + { + if ($this->config === null) { + $this->setConfiguration(Configuration::getInstance()); + } + if ($this->session === null) { + $this->setSession(Session::getSessionFromRequest()); + } + $this->request = $request; + if ($request === null) { + $this->request = Request::createFromGlobals(); + } + $stack = new RequestStack(); + $stack->push($this->request); + $this->context->fromRequest($this->request); + $kernel = new HttpKernel($this->dispatcher, $this->resolver, $stack, $this->resolver); + return $kernel->handle($this->request); + } + + + /** + * Send a given response to the browser. + * + * @param Response $response The response to send. + */ + public function send(Response $response) + { + $response->prepare($this->request); + $response->send(); + } + + + /** + * Set the configuration to use by the controller. + * + * @param \SimpleSAML\Configuration $config + */ + public function setConfiguration(Configuration $config) + { + $this->config = $config; + $this->resolver->setConfiguration($config); + } + + + /** + * Set the session to use by the controller. + * + * @param \SimpleSAML\Session $session + */ + public function setSession(Session $session) + { + $this->session = $session; + $this->resolver->setSession($session); + } +} diff --git a/lib/SimpleSAML/HTTP/RunnableResponse.php b/lib/SimpleSAML/HTTP/RunnableResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..ab9fc6c5f14ed6b31a7c16522de1b5e1033bd227 --- /dev/null +++ b/lib/SimpleSAML/HTTP/RunnableResponse.php @@ -0,0 +1,69 @@ +<?php + +namespace SimpleSAML\HTTP; + +use Symfony\Component\HttpFoundation\Response; + +/** + * Class modelling a response that consists on running some function. + * + * This is a helper class that allows us to have the new and the old architecture coexist. This way, classes and files + * that aren't PSR-7-aware can still be plugged into a PSR-7-compatible environment. + * + * @package SimpleSAML + */ +class RunnableResponse extends Response +{ + /** @var array */ + protected $arguments; + + /** @var callable */ + protected $callable; + + + /** + * RunnableResponse constructor. + * + * @param callable $callable A callable that we should run as part of this response. + * @param array $args An array of arguments to be passed to the callable. Note that each element of the array + */ + public function __construct(callable $callable, $args = []) + { + $this->arguments = $args; + $this->callable = $callable; + parent::__construct(); + } + + + /** + * Get the callable for this response. + * + * @return callable + */ + public function getCallable() + { + return $this->callable; + } + + + /** + * Get the arguments to the callable. + * + * @return array + */ + public function getArguments() + { + return $this->arguments; + } + + + /** + * "Send" this response by actually running the callable. + * + * @return mixed + */ + public function send() + { + return call_user_func_array($this->callable, $this->arguments); + } +} diff --git a/lib/SimpleSAML/Locale/Localization.php b/lib/SimpleSAML/Locale/Localization.php index 9a4543d37ae95fc21553d8653eaf39867440179d..66427da457578cc1cb8ea9e4f398bf0e1d3bae58 100644 --- a/lib/SimpleSAML/Locale/Localization.php +++ b/lib/SimpleSAML/Locale/Localization.php @@ -96,7 +96,7 @@ class Localization $this->localeDir = $this->configuration->resolvePath('locales'); $this->language = new Language($configuration); $this->langcode = $this->language->getPosixLanguage($this->language->getLanguage()); - $this->i18nBackend = $this->configuration->getString('language.i18n.backend', self::SSP_I18N_BACKEND); + $this->i18nBackend = ($this->configuration->getBoolean('usenewui', false) ? self::GETTEXT_I18N_BACKEND : self::SSP_I18N_BACKEND); $this->setupL10N(); } diff --git a/lib/SimpleSAML/Module.php b/lib/SimpleSAML/Module.php index 9668a367306afef04e8a10f8cd7561d925456fdc..5e1488ff07b828cf2529bbde9ee09e593df0ee86 100644 --- a/lib/SimpleSAML/Module.php +++ b/lib/SimpleSAML/Module.php @@ -2,6 +2,12 @@ namespace SimpleSAML; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; + /** * Helper class for accessing information about modules. * @@ -12,6 +18,44 @@ namespace SimpleSAML; */ class Module { + + /** + * Index pages: file names to attempt when accessing directories. + * + * @var array + */ + public static $indexFiles = ['index.php', 'index.html', 'index.htm', 'index.txt']; + + /** + * MIME Types + * + * The key is the file extension and the value the corresponding MIME type. + * + * @var array + */ + public static $mimeTypes = [ + 'bmp' => 'image/x-ms-bmp', + 'css' => 'text/css', + 'gif' => 'image/gif', + 'htm' => 'text/html', + 'html' => 'text/html', + 'shtml' => 'text/html', + 'ico' => 'image/vnd.microsoft.icon', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'text/javascript', + 'pdf' => 'application/pdf', + 'png' => 'image/png', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 'swfl' => 'application/x-shockwave-flash', + 'txt' => 'text/plain', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + ]; + /** * A list containing the modules currently installed. * @@ -63,6 +107,172 @@ class Module } + /** + * Handler for module requests. + * + * This controller receives requests for pages hosted by modules, and processes accordingly. Depending on the + * configuration and the actual request, it will run a PHP script and exit, or return a Response produced either + * by another controller or by a static file. + * + * @param Request|null $request The request to process. Defaults to the current one. + * + * @return Response|BinaryFileResponse Returns a Response object that can be sent to the browser. + * @throws Error\BadRequest In case the request URI is malformed. + * @throws Error\NotFound In case the request URI is invalid or the resource it points to cannot be found. + */ + public static function process(Request $request = null) + { + if ($request === null) { + $request = Request::createFromGlobals(); + } + + if ($request->getPathInfo() === '/') { + throw new Error\NotFound('No PATH_INFO to module.php'); + } + + $url = $request->getPathInfo(); + assert(substr($url, 0, 1) === '/'); + + /* clear the PATH_INFO option, so that a script can detect whether it is called with anything following the + *'.php'-ending. + */ + unset($_SERVER['PATH_INFO']); + + $modEnd = strpos($url, '/', 1); + if ($modEnd === false) { + // the path must always be on the form /module/ + throw new Error\NotFound('The URL must at least contain a module name followed by a slash.'); + } + + $module = substr($url, 1, $modEnd - 1); + $url = substr($url, $modEnd + 1); + if ($url === false) { + $url = ''; + } + + if (!self::isModuleEnabled($module)) { + throw new Error\NotFound('The module \''.$module.'\' was either not found, or wasn\'t enabled.'); + } + + /* Make sure that the request isn't suspicious (contains references to current directory or parent directory or + * anything like that. Searching for './' in the URL will detect both '../' and './'. Searching for '\' will + * detect attempts to use Windows-style paths. + */ + if (strpos($url, '\\') !== false) { + throw new Error\BadRequest('Requested URL contained a backslash.'); + } elseif (strpos($url, './') !== false) { + throw new Error\BadRequest('Requested URL contained \'./\'.'); + } + + $config = Configuration::getInstance(); + if ($config->getBoolean('usenewui', false) === true) { + $router = new HTTP\Router($module); + try { + return $router->process(); + } catch (\Symfony\Component\Config\Exception\FileLocatorFileNotFoundException $e) { + // no routes configured for this module, fall back to the old system + } catch (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) { + // this module has been migrated, but the route wasn't found + } + } + + $moduleDir = self::getModuleDir($module).'/www/'; + + // check for '.php/' in the path, the presence of which indicates that another php-script should handle the + // request + for ($phpPos = strpos($url, '.php/'); $phpPos !== false; $phpPos = strpos($url, '.php/', $phpPos + 1)) { + $newURL = substr($url, 0, $phpPos + 4); + $param = substr($url, $phpPos + 4); + + if (is_file($moduleDir.$newURL)) { + /* $newPath points to a normal file. Point execution to that file, and save the remainder of the path + * in PATH_INFO. + */ + $url = $newURL; + $request->server->set('PATH_INFO', $param); + $_SERVER['PATH_INFO'] = $param; + break; + } + } + + $path = $moduleDir.$url; + + if ($path[strlen($path) - 1] === '/') { + // path ends with a slash - directory reference. Attempt to find index file in directory + foreach (self::$indexFiles as $if) { + if (file_exists($path.$if)) { + $path .= $if; + break; + } + } + } + + if (is_dir($path)) { + /* Path is a directory - maybe no index file was found in the previous step, or maybe the path didn't end + * with a slash. Either way, we don't do directory listings. + */ + throw new Error\NotFound('Directory listing not available.'); + } + + if (!file_exists($path)) { + // file not found + Logger::info('Could not find file \''.$path.'\'.'); + throw new Error\NotFound('The URL wasn\'t found in the module.'); + } + + if (substr($path, -4) === '.php') { + // PHP file - attempt to run it + + /* In some environments, $_SERVER['SCRIPT_NAME'] is already set with $_SERVER['PATH_INFO']. Check for that + * case, and append script name only if necessary. + * + * Contributed by Travis Hegner. + */ + $script = "/$module/$url"; + if (strpos($request->getScriptName(), $script) === false) { + $request->server->set('SCRIPT_NAME', $request->getScriptName().'/'.$module.'/'.$url); + } + + require($path); + exit(); + } + + // some other file type - attempt to serve it + + // find MIME type for file, based on extension + $contentType = null; + if (preg_match('#\.([^/\.]+)$#D', $path, $type)) { + $type = strtolower($type[1]); + if (array_key_exists($type, self::$mimeTypes)) { + $contentType = self::$mimeTypes[$type]; + } + } + + if ($contentType === null) { + /* We were unable to determine the MIME type from the file extension. Fall back to mime_content_type (if it + * exists). + */ + if (function_exists('mime_content_type')) { + $contentType = mime_content_type($path); + } else { + // mime_content_type doesn't exist. Return a default MIME type + Logger::warning('Unable to determine mime content type of file: '.$path); + $contentType = 'application/octet-stream'; + } + } + + $response = new BinaryFileResponse($path); + $response->setCache(['public' => true, 'max_age' => 86400]); + $response->setExpires(new \DateTime(gmdate('D, j M Y H:i:s \G\M\T', time() + 10 * 60))); + $response->setLastModified(new \DateTime(gmdate('D, j M Y H:i:s \G\M\T', filemtime($path)))); + $response->headers->set('Content-Type', $contentType); + $response->headers->set('Content-Length', sprintf('%u', filesize($path))); // force file size to an unsigned + $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE); + $response->prepare($request); + return $response; + } + + private static function isModuleEnabledWithConf($module, $mod_config) { if (isset(self::$module_info[$module]['enabled'])) { @@ -310,4 +520,21 @@ class Module $fn($data); } } + + + /** + * Handle a valid request that ends with a trailing slash. + * + * This method removes the trailing slash and redirects to the resulting URL. + * + * @param Request $request The request to process by this controller method. + * + * @return RedirectResponse A redirection to the URI specified in the request, but without the trailing slash. + */ + public static function removeTrailingSlash(Request $request) + { + $pathInfo = $request->getPathInfo(); + $url = str_replace($pathInfo, rtrim($pathInfo, ' /'), $request->getRequestUri()); + return new RedirectResponse($url, 308); + } } diff --git a/lib/SimpleSAML/Module/ControllerResolver.php b/lib/SimpleSAML/Module/ControllerResolver.php new file mode 100644 index 0000000000000000000000000000000000000000..ba79109d100dde7da2701eecf4b6934744cf1292 --- /dev/null +++ b/lib/SimpleSAML/Module/ControllerResolver.php @@ -0,0 +1,196 @@ +<?php + +namespace SimpleSAML\Module; + +use SimpleSAML\Auth\AuthenticationFactory; +use SimpleSAML\Configuration; +use SimpleSAML\Error\Exception; +use SimpleSAML\Module; +use SimpleSAML\Session; + +use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; +use Symfony\Component\HttpKernel\Controller\ControllerResolver as SymfonyControllerResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Loader\YamlFileLoader; +use Symfony\Component\Routing\Matcher\UrlMatcher; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * A class to resolve module controllers based on a given request. + * + * This class allows us to find a controller (a callable) that's configured for a given URL. + * + * @package SimpleSAML + */ +class ControllerResolver extends SymfonyControllerResolver implements ArgumentResolverInterface +{ + + /** @var ArgumentMetadataFactory */ + protected $argFactory; + + /** @var ContainerBuilder */ + protected $container; + + /** @var string */ + protected $module; + + /** @var array */ + protected $params; + + /** @var RouteCollection */ + protected $routes; + + + /** + * Build a module controller resolver. + * + * @param string $module The name of the module. + */ + public function __construct($module) + { + parent::__construct(); + $this->module = $module; + + $loader = new YamlFileLoader( + new FileLocator(Module::getModuleDir($this->module)) + ); + + $this->argFactory = new ArgumentMetadataFactory(); + $this->container = new ContainerBuilder(); + $this->container->autowire(AuthenticationFactory::class, AuthenticationFactory::class); + + try { + $this->routes = $loader->load('routes.yaml'); + $redirect = new Route( + '/{url}', + ['_controller' => '\SimpleSAML\Module::removeTrailingSlash'], + ['url' => '.*/$'] + ); + $this->routes->add('trailing-slash', $redirect); + $this->routes->addPrefix('/'.$this->module); + } catch (FileLocatorFileNotFoundException $e) { + } + } + + + /** + * Get the controller associated with a given URL, based on a request. + * + * This method searches for a 'routes.yaml' file in the root of the module, defining valid routes for the module + * and mapping them given controllers. It's input is a Request object with the request that we want to serve. + * + * @param Request $request The request we need to find a controller for. + * + * @return callable|false A controller (as a callable) that can handle the request, or false if we cannot find + * one suitable for the given request. + */ + public function getController(Request $request) + { + if ($this->routes === null) { + return false; + } + $ctxt = new RequestContext(); + $ctxt->fromRequest($request); + + try { + $matcher = new UrlMatcher($this->routes, $ctxt); + $this->params = $matcher->match($ctxt->getPathInfo()); + list($class, $method) = explode('::', $this->params['_controller']); + $this->container->register($class, $class)->setAutowired(true); + $this->container->compile(); + return [$this->container->get($class), $method]; + } catch (ResourceNotFoundException $e) { + // no route defined matching this request + } + return false; + } + + + /** + * Get the arguments that should be passed to a controller from a given request. + * + * When the signature of the controller includes arguments with type Request, the given request will be passed to + * those. Otherwise, they'll be matched by name. If no value is available for a given argument, the method will + * try to set a default value or null, if possible. + * + * @param Request $request The request that holds all the information needed by the controller. + * @param callable $controller A controller for the given request. + * + * @return array An array of arguments that should be passed to the controller, in order. + * + * @throws \SimpleSAML\Error\Exception If we don't find anything suitable for an argument in the controller's + * signature. + */ + public function getArguments(Request $request, $controller) + { + $args = []; + $metadata = $this->argFactory->createArgumentMetadata($controller); + + /** @var ArgumentMetadata $argMeta */ + foreach ($metadata as $argMeta) { + if ($argMeta->getType() === 'Symfony\Component\HttpFoundation\Request') { + // add request argument + $args[] = $request; + continue; + } + + $argName = $argMeta->getName(); + if (array_key_exists($argName, $this->params)) { + // add argument by name + $args[] = $this->params[$argName]; + continue; + } + + // URL does not contain value for this argument + if ($argMeta->hasDefaultValue()) { + // it has a default value + $args[] = $argMeta->getDefaultValue(); + } + + // no default value + if ($argMeta->isNullable()) { + $args[] = null; + } + + throw new Exception('Missing value for argument '.$argName.'. This is probably a bug.'); + } + + return $args; + } + + + /** + * Set the configuration to use by the controllers. + * + * @param \SimpleSAML\Configuration $config + */ + public function setConfiguration(Configuration $config) + { + $this->container->set(Configuration::class, $config); + $this->container->register(Configuration::class)->setSynthetic(true)->setAutowired(true); + } + + + /** + * Set the session to use by the controllers. + * + * @param \SimpleSAML\Session $session + */ + public function setSession(Session $session) + { + $this->container->set(Session::class, $session); + $this->container->register(Session::class) + ->setSynthetic(true) + ->setAutowired(true) + ->addMethodCall('setConfiguration', [new Reference(Configuration::class)]); + } +} diff --git a/lib/SimpleSAML/Session.php b/lib/SimpleSAML/Session.php index 518f16294ade68b796b2141ac54186ac85fae9e3..62b0a52e3bf0d50a8c131bce0c4a582cc016c7d6 100644 --- a/lib/SimpleSAML/Session.php +++ b/lib/SimpleSAML/Session.php @@ -22,7 +22,7 @@ use SimpleSAML\Error; * @package SimpleSAMLphp */ -class Session implements \Serializable +class Session implements \Serializable, Utils\ClearableState { /** * This is a timeout value for setData, which indicates that the data @@ -48,6 +48,13 @@ class Session implements \Serializable */ private static $instance = null; + /** + * The global configuration. + * + * @var \SimpleSAML\Configuration + */ + private static $config; + /** * The session ID of this session. * @@ -131,6 +138,7 @@ class Session implements \Serializable */ private $authData = []; + /** * Private constructor that restricts instantiation to either getSessionFromRequest() for the current session or * getSession() for a specific one. @@ -139,6 +147,8 @@ class Session implements \Serializable */ private function __construct($transient = false) { + $this->setConfiguration(Configuration::getInstance()); + if (php_sapi_name() === 'cli' || defined('STDIN')) { $this->trackid = 'CL'.bin2hex(openssl_random_pseudo_bytes(4)); Logger::setTrackId($this->trackid); @@ -174,8 +184,7 @@ class Session implements \Serializable $this->markDirty(); // initialize data for session check function if defined - $globalConfig = Configuration::getInstance(); - $checkFunction = $globalConfig->getArray('session.check_function', null); + $checkFunction = self::$config->getArray('session.check_function', null); if (isset($checkFunction)) { assert(is_callable($checkFunction)); call_user_func($checkFunction, $this, true); @@ -183,6 +192,18 @@ class Session implements \Serializable } } + + /** + * Set the configuration we should use. + * + * @param Configuration $config + */ + public function setConfiguration(Configuration $config) + { + self::$config = $config; + } + + /** * Serialize this session object. * @@ -192,8 +213,7 @@ class Session implements \Serializable */ public function serialize() { - $serialized = serialize(get_object_vars($this)); - return $serialized; + return serialize(get_object_vars($this)); } /** @@ -212,6 +232,7 @@ class Session implements \Serializable $this->$k = $v; } } + self::$config = Configuration::getInstance(); // look for any raw attributes and load them in the 'Attributes' array foreach ($this->authData as $authority => $parameters) { @@ -542,8 +563,7 @@ class Session implements \Serializable assert(is_int($expire) || $expire === null); if ($expire === null) { - $globalConfig = Configuration::getInstance(); - $expire = time() + $globalConfig->getInteger('session.rememberme.lifetime', 14 * 86400); + $expire = time() + self::$config->getInteger('session.rememberme.lifetime', 14 * 86400); } $this->rememberMeExpire = $expire; @@ -581,12 +601,11 @@ class Session implements \Serializable $data['Authority'] = $authority; - $globalConfig = Configuration::getInstance(); if (!isset($data['AuthnInstant'])) { $data['AuthnInstant'] = time(); } - $maxSessionExpire = time() + $globalConfig->getInteger('session.duration', 8 * 60 * 60); + $maxSessionExpire = time() + self::$config->getInteger('session.duration', 8 * 60 * 60); if (!isset($data['Expire']) || $data['Expire'] > $maxSessionExpire) { // unset, or beyond our session lifetime. Clamp it to our maximum session lifetime $data['Expire'] = $maxSessionExpire; @@ -621,13 +640,13 @@ class Session implements \Serializable $sessionHandler = SessionHandler::getSessionHandler(); if (!$this->transient && (!empty($data['RememberMe']) || $this->rememberMeExpire) && - $globalConfig->getBoolean('session.rememberme.enable', false) + self::$config->getBoolean('session.rememberme.enable', false) ) { $this->setRememberMeExpire(); } else { try { Utils\HTTP::setCookie( - $globalConfig->getString('session.authtoken.cookiename', 'SimpleSAMLAuthToken'), + self::$config->getString('session.authtoken.cookiename', 'SimpleSAMLAuthToken'), $this->authToken, $sessionHandler->getCookieParams() ); @@ -755,9 +774,8 @@ class Session implements \Serializable $params = array_merge($sessionHandler->getCookieParams(), is_array($params) ? $params : []); if ($this->authToken !== null) { - $globalConfig = Configuration::getInstance(); Utils\HTTP::setCookie( - $globalConfig->getString('session.authtoken.cookiename', 'SimpleSAMLAuthToken'), + self::$config->getString('session.authtoken.cookiename', 'SimpleSAMLAuthToken'), $this->authToken, $params ); @@ -778,8 +796,7 @@ class Session implements \Serializable $this->markDirty(); if ($expire === null) { - $globalConfig = Configuration::getInstance(); - $expire = time() + $globalConfig->getInteger('session.duration', 8 * 60 * 60); + $expire = time() + self::$config->getInteger('session.duration', 8 * 60 * 60); } $this->authData[$authority]['Expire'] = $expire; @@ -859,9 +876,7 @@ class Session implements \Serializable if ($timeout === null) { // use the default timeout - $configuration = Configuration::getInstance(); - - $timeout = $configuration->getInteger('session.datastore.timeout', null); + $timeout = self::$config->getInteger('session.datastore.timeout', null); if ($timeout !== null) { if ($timeout <= 0) { throw new \Exception( @@ -1134,4 +1149,15 @@ class Session implements \Serializable } return $authorities; } + + + /** + * Clear any configuration information cached + */ + public static function clearInternalState() + { + self::$config = null; + self::$instance = null; + self::$sessions = null; + } } diff --git a/lib/SimpleSAML/Store.php b/lib/SimpleSAML/Store.php index d5be90656fd91737839453c198b00a9bfa864c4d..7f227fca2a37000354e6705b2b5622379b07df36 100644 --- a/lib/SimpleSAML/Store.php +++ b/lib/SimpleSAML/Store.php @@ -9,7 +9,7 @@ use SimpleSAML\Error\CriticalConfigurationError; * * @package SimpleSAMLphp */ -abstract class Store +abstract class Store implements Utils\ClearableState { /** * Our singleton instance. @@ -100,4 +100,13 @@ abstract class Store * @param string $key The key. */ abstract public function delete($type, $key); + + + /** + * Clear any SSP specific state, such as SSP environmental variables or cached internals. + */ + public static function clearInternalState() + { + self::$instance = null; + } } diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php index 39bce25ebd516feace6cb381c946e3508e2dd522..903076ff67f701a2d66f6bcb691e8305fb1b69d5 100644 --- a/lib/SimpleSAML/XHTML/Template.php +++ b/lib/SimpleSAML/XHTML/Template.php @@ -11,8 +11,10 @@ namespace SimpleSAML\XHTML; use JaimePerez\TwigConfigurableI18n\Twig\Environment as Twig_Environment; use JaimePerez\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n; +use Symfony\Component\HttpFoundation\Response; +use SimpleSAML\Locale\Localization; -class Template +class Template extends Response { /** * The data associated with this template, accessible within the template itself. @@ -68,6 +70,14 @@ class Template */ private $module; + /** + * Whether to use the new user interface or not. + * + * @var bool + */ + private $useNewUI; + + /** * A template controller, if any. * @@ -84,10 +94,10 @@ class Template * Whether we are using a non-default theme or not. * * If we are using a theme, this variable holds an array with two keys: "module" and "name", those being the name - * of the module and the name of the theme, respectively. If we are using the default theme, the variable defaults - * to false. + * of the module and the name of the theme, respectively. If we are using the default theme, the variable has + * the 'default' string in the "name" key, and 'null' in the "module" key. * - * @var bool|array + * @var array */ private $theme; @@ -117,6 +127,9 @@ class Template $this->translator = new \SimpleSAML\Locale\Translate($configuration, $defaultDictionary); $this->localization = new \SimpleSAML\Locale\Localization($configuration); + // check if we are supposed to use the new UI + $this->useNewUI = $this->configuration->getBoolean('usenewui', false); + // check if we need to attach a theme controller $controller = $this->configuration->getString('theme.controller', false); if ($controller && class_exists($controller) && @@ -126,6 +139,18 @@ class Template } $this->twig = $this->setupTwig(); + parent::__construct(); + } + + + /** + * Get the normalized template name. + * + * @return string The name of the template to use. + */ + public function getTemplateName() + { + return $this->normalizeTemplateName($this->template); } @@ -148,7 +173,11 @@ class Template if ($tplpos) { $templateName = substr($templateName, 0, $tplpos); } - return $templateName.'.twig'; + + if ($this->useNewUI || $this->theme['module'] !== null) { + return $templateName.'.twig'; + } + return $templateName; } @@ -237,7 +266,7 @@ class Template // initialize some basic context $langParam = $this->configuration->getString('language.parameter.name', 'language'); $twig->addGlobal('languageParameterName', $langParam); - $twig->addGlobal('localeBackend', $this->configuration->getString('language.i18n.backend', 'SimpleSAMLphp')); + $twig->addGlobal('localeBackend', $this->useNewUI ? Localization::GETTEXT_I18N_BACKEND : Localization::SSP_I18N_BACKEND); $twig->addGlobal('currentLanguage', $this->translator->getLanguage()->getLanguage()); $twig->addGlobal('isRTL', false); // language RTL configuration if ($this->translator->getLanguage()->isLanguageRTL()) { @@ -409,20 +438,50 @@ class Template /** - * Show the template to the user. + * Get the contents produced by this template. + * + * @return string The HTML rendered by this template, as a string. + * @throws \Exception if the template cannot be found. */ - public function show() + protected function getContents() { if ($this->twig !== false) { $this->twigDefaultContext(); if ($this->controller) { $this->controller->display($this->data); } - echo $this->twig->render($this->twig_template, $this->data); + $content = $this->twig->render($this->twig_template, $this->data); } else { - $filename = $this->findTemplatePath($this->template); - require($filename); + $content = require($this->findTemplatePath($this->template)); } + return $content; + } + + + /** + * Send this template as a response. + * + * @return Response This response. + * @throws \Exception if the template cannot be found. + */ + public function send() + { + $this->content = $this->getContents(); + return parent::send(); + } + + + /** + * Show the template to the user. + * + * This method is a remnant of the old templating system, where templates where shown manually instead of + * returning a response. + * + * @deprecated Do not use this method, use send() instead. + */ + public function show() + { + echo $this->getContents(); } diff --git a/modules/core/lib/Controller.php b/modules/core/lib/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..8a4563f42df18c96261dab9a35344e64c8a01c7e --- /dev/null +++ b/modules/core/lib/Controller.php @@ -0,0 +1,174 @@ +<?php + +namespace SimpleSAML\Module\core; + +use SimpleSAML\Error\Exception; +use SimpleSAML\HTTP\RunnableResponse; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; + +/** + * Controller class for the core module. + * + * This class serves the different views available in the module. + * + * @package SimpleSAML\Module\core + */ +class Controller +{ + + /** @var \SimpleSAML\Configuration */ + protected $config; + + /** @var \SimpleSAML\Auth\AuthenticationFactory */ + protected $factory; + + /** @var \SimpleSAML\Session */ + protected $session; + + /** @var array */ + protected $sources; + + + /** + * Controller constructor. + * + * It initializes the global configuration and auth source 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. + * @param \SimpleSAML\Auth\AuthenticationFactory $factory A factory to instantiate \SimpleSAML\Auth\Simple objects. + * + * @throws \Exception + */ + public function __construct( + \SimpleSAML\Configuration $config, + \SimpleSAML\Session $session, + \SimpleSAML\Auth\AuthenticationFactory $factory + ) { + $this->config = $config; + $this->factory = $factory; + $this->sources = $config::getOptionalConfig('authsources.php')->toArray(); + $this->session = $session; + } + + + /** + * Show account information for a given authentication source. + * + * @param string $as The identifier of the authentication source. + * + * @return \SimpleSAML\XHTML\Template|RedirectResponse An HTML template or a redirection if we are not + * authenticated. + * + * @throws \SimpleSAML\Error\Exception An exception in case the auth source specified is invalid. + */ + public function account($as) + { + if (!array_key_exists($as, $this->sources)) { + throw new Exception('Invalid authentication source'); + } + + $auth = $this->factory->create($as); + if (!$auth->isAuthenticated()) { + // not authenticated, start auth with specified source + return new RedirectResponse(\SimpleSAML\Module::getModuleURL('core/login/'.urlencode($as))); + } + + $attributes = $auth->getAttributes(); + + $t = new \SimpleSAML\XHTML\Template($this->config, 'auth_status.twig', 'attributes'); + $t->data['header'] = '{status:header_saml20_sp}'; + $t->data['attributes'] = $attributes; + $t->data['nameid'] = !is_null($auth->getAuthData('saml:sp:NameID')) + ? $auth->getAuthData('saml:sp:NameID') + : false; + $t->data['logouturl'] = \SimpleSAML\Module::getModuleURL('core/logout/'.urlencode($as)); + $t->data['remaining'] = $this->session->getAuthData($as, 'Expire') - time(); + $t->setStatusCode(200); + + return $t; + } + + + /** + * Perform a login operation. + * + * This controller will either start a login operation (if that was requested, or if only one authentication + * source is available), or show a template allowing the user to choose which auth source to use. + * + * @param Request $request The request that lead to this login operation. + * @param string|null $as The name of the authentication source to use, if any. Optional. + * + * @return \SimpleSAML\XHTML\Template|\SimpleSAML\HTTP\RunnableResponse|RedirectResponse An HTML template, a + * redirect or a "runnable" response. + * + * @throws \SimpleSAML\Error\Exception + */ + public function login(Request $request, $as = null) + { + //delete admin + if (isset($this->sources['admin'])) { + unset($this->sources['admin']); + } + + if (count($this->sources) === 1 && $as === null) { // we only have one source available + $as = key($this->sources); + } + + if ($as === null) { // no authentication source specified + $t = new \SimpleSAML\XHTML\Template($this->config, 'core:login.twig'); + $t->data['loginurl'] = \SimpleSAML\Utils\Auth::getAdminLoginURL(); + $t->data['sources'] = $this->sources; + return $t; + } + + // auth source defined, check if valid + if (!array_key_exists($as, $this->sources)) { + throw new Exception('Invalid authentication source'); + } + + // at this point, we have a valid auth source selected, start auth + $auth = $this->factory->create($as); + $as = urlencode($as); + + if ($request->get(\SimpleSAML\Auth\State::EXCEPTION_PARAM, false) !== false) { + // This is just a simple example of an error + + $state = \SimpleSAML\Auth\State::loadExceptionState(); + assert(array_key_exists(\SimpleSAML\Auth\State::EXCEPTION_DATA, $state)); + $e = $state[\SimpleSAML\Auth\State::EXCEPTION_DATA]; + + throw $e; + } + + if ($auth->isAuthenticated()) { + return new RedirectResponse(\SimpleSAML\Module::getModuleURL('core/account/'.$as)); + } + + // we're not logged in, start auth + $url = \SimpleSAML\Module::getModuleURL('core/login/'.$as); + $params = array( + 'ErrorURL' => $url, + 'ReturnTo' => $url, + ); + return new RunnableResponse([$auth, 'login'], [$params]); + } + + + /** + * Log the user out of a given authentication source. + * + * @param string $as The name of the auth source. + * + * @return \SimpleSAML\HTTP\RunnableResponse A runnable response which will actually perform logout. + * + * @throws \SimpleSAML\Error\CriticalConfigurationError + */ + public function logout($as) + { + $auth = new \SimpleSAML\Auth\Simple($as); + return new RunnableResponse([$auth, 'logout'], [$this->config->getBasePath().'logout.php']); + } +} diff --git a/modules/core/routes.yaml b/modules/core/routes.yaml new file mode 100644 index 0000000000000000000000000000000000000000..80916d2ab51c7a39b650b5c841983f0ba4a629f2 --- /dev/null +++ b/modules/core/routes.yaml @@ -0,0 +1,9 @@ +core-login: + path: /login/{as} + defaults: { _controller: 'SimpleSAML\Module\core\Controller::login', as: null } +core-account: + path: /account/{as} + defaults: { _controller: 'SimpleSAML\Module\core\Controller::account' } +core-logout: + path: /logout/{as} + defaults: { _controller: 'SimpleSAML\Module\core\Controller::logout' } diff --git a/modules/core/templates/login.twig b/modules/core/templates/login.twig index e1b342cc0a9ba9e71324930c7ff732f2e69f860e..03297b97192abad0c08f0d111da7be3ec87d9b54 100644 --- a/modules/core/templates/login.twig +++ b/modules/core/templates/login.twig @@ -12,7 +12,11 @@ <div class="pure-menu custom-restricted-width"> <ul class="pure-menu-list auth_methods"> {% for id, config in sources -%} - <li class="pure-menu-item"><a href="?as={{ id|url_encode }}" class="pure-menu-link">{{ config.name|translateFromArray|default(id) }}</a></li> + <li class="pure-menu-item"> + <a href="{{ baseurlpath }}/module.php/core/login/{{ id|url_encode }}" class="pure-menu-link"> + {{ config.name|translateFromArray|default(id) }} + </a> + </li> {% endfor -%} </ul> </div> diff --git a/modules/core/www/frontpage_auth.php b/modules/core/www/frontpage_auth.php index 081151788e230f0fccedf24d7e9f1cbb0b51b9a4..a3bf112e150efee435323163208942432d79ee9c 100644 --- a/modules/core/www/frontpage_auth.php +++ b/modules/core/www/frontpage_auth.php @@ -19,7 +19,7 @@ $links_auth = []; $links_federation = []; $links_auth[] = [ - 'href' => 'login.php', + 'href' => 'authenticate.php', 'text' => '{core:frontpage:authtest}', ]; diff --git a/modules/core/www/login.php b/modules/core/www/login.php deleted file mode 100644 index 53d57ca44976bbc805c2df889255182449eabe98..0000000000000000000000000000000000000000 --- a/modules/core/www/login.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php - -$config = \SimpleSAML\Configuration::getInstance(); -$sources = \SimpleSAML\Configuration::getOptionalConfig('authsources.php')->toArray(); - -//delete admin -if (isset($sources['admin'])) { - unset($sources['admin']); -} - -//if only 1 auth -if (count($sources) == 1) { - $_REQUEST['as'] = key($sources); -} - -if (!array_key_exists('as', $_REQUEST)) { - $t = new \SimpleSAML\XHTML\Template($config, 'core:login.twig'); - - $t->data['loginurl'] = \SimpleSAML\Utils\Auth::getAdminLoginURL(); - $t->data['sources'] = $sources; - $t->show(); - exit(); -} - -$asId = (string) $_REQUEST['as']; -$as = new \SimpleSAML\Auth\Simple($asId); - -if (array_key_exists('logout', $_REQUEST)) { - $as->logout($config->getBasePath().'logout.php'); -} - -if (array_key_exists(\SimpleSAML\Auth\State::EXCEPTION_PARAM, $_REQUEST)) { - // This is just a simple example of an error - - $state = \SimpleSAML\Auth\State::loadExceptionState(); - assert(array_key_exists(\SimpleSAML\Auth\State::EXCEPTION_DATA, $state)); - $e = $state[\SimpleSAML\Auth\State::EXCEPTION_DATA]; - - throw $e; -} - -if (!$as->isAuthenticated()) { - $url = \SimpleSAML\Module::getModuleURL('core/login.php', ['as' => $asId]); - $params = [ - 'ErrorURL' => $url, - 'ReturnTo' => $url, - ]; - $as->login($params); -} - -$attributes = $as->getAttributes(); -$session = \SimpleSAML\Session::getSessionFromRequest(); - -$t = new \SimpleSAML\XHTML\Template($config, 'auth_status.twig', 'attributes'); - - -$t->data['header'] = '{status:header_saml20_sp}'; -$t->data['attributes'] = $attributes; -$t->data['nameid'] = !is_null($as->getAuthData('saml:sp:NameID')) ? $as->getAuthData('saml:sp:NameID') : false; -$t->data['logouturl'] = \SimpleSAML\Utils\HTTP::getSelfURLNoQuery().'?as='.urlencode($asId).'&logout'; -$t->data['remaining'] = $session->getAuthData($asId, 'Expire') - time(); - -$t->show(); diff --git a/templates/sandbox.twig b/templates/sandbox.twig index 316c855cfba98e398a8237ed4dc77567505efdf5..1ff8134fd7e0a2d08a1b1be18f7623549cce2ef3 100644 --- a/templates/sandbox.twig +++ b/templates/sandbox.twig @@ -15,17 +15,6 @@ </p> <h2>Localization</h2> {% set variable = 'Hello, Untranslated World!' %} - <p>SimpleSAMLphp lets you choose which translation backend to choose, thanks to the - <code>language.i18n.backend</code> configuration option. Two possible values are supported there: - </p> - <ul> - <li><code>SimpleSAMLphp</code>: to keep using the old SimpleSAMLphp translation system. This is the - default, and will disappear as an option in SimpleSAMLphp 2.0.</li> - <li><code>gettext/gettext</code>: to use a <em>gettext</em> implementation written entirely in PHP, allowing - you to use any locale, no matter if they are installed on the system or not.</li> - </ul> - <p>Note that <code>gettext/gettext</code> <strong>will become the default</strong> in SimpleSAMLphp 2.0. - Currently, you are using the following backend: <code>{{ localeBackend }}</code>.</p> <p>This page is written in english only, but the examples used here are translated to several languages. The current language is <strong>{{ currentLanguage }}</strong>. Change to other languages to see the examples change.</p> <h4>Usage examples</h4> diff --git a/tests/Utils/StateClearer.php b/tests/Utils/StateClearer.php index 880c66bdf6332969503f23ec23743d7d0e6868b7..9472ef1993e6b18f675d8cd72df420b99b608edd 100644 --- a/tests/Utils/StateClearer.php +++ b/tests/Utils/StateClearer.php @@ -18,7 +18,7 @@ class StateClearer * Class that implement \SimpleSAML\Utils\ClearableState and should have clearInternalState called between tests * @var array */ - private $clearableState = ['SimpleSAML\Configuration', 'SimpleSAML\Metadata\MetaDataStorageHandler']; + private $clearableState = ['SimpleSAML\Configuration', 'SimpleSAML\Metadata\MetaDataStorageHandler', 'SimpleSAML\Store', 'SimpleSAML\Session']; /** * Environmental variables to unset diff --git a/tests/lib/SimpleSAML/Locale/LocalizationTest.php b/tests/lib/SimpleSAML/Locale/LocalizationTest.php index cc58f1ebe66632efcd6ffac4b9d4ce21e5391c8a..ceeb823fba12480b83cf812efed375eee9fc3b48 100644 --- a/tests/lib/SimpleSAML/Locale/LocalizationTest.php +++ b/tests/lib/SimpleSAML/Locale/LocalizationTest.php @@ -22,7 +22,7 @@ class LocalizationTest extends TestCase { // The constructor should activate the default domain $c = Configuration::loadFromArray( - ['language.i18n.backend' => 'SimpleSAMLphp'] + ['usenewui' => false] ); $l = new Localization($c); $this->assertTrue($l->isI18NBackendDefault()); @@ -35,7 +35,7 @@ class LocalizationTest extends TestCase public function testAddDomain() { $c = Configuration::loadFromArray( - ['language.i18n.backend' => 'gettext/gettext'] + ['usenewui' => true] ); $l = new Localization($c); $newDomain = 'test'; diff --git a/tests/modules/core/lib/ControllerTest.php b/tests/modules/core/lib/ControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9f2edea91b9409784e2b0f97e2161d634d32fca5 --- /dev/null +++ b/tests/modules/core/lib/ControllerTest.php @@ -0,0 +1,244 @@ +<?php + +namespace SimpleSAML\Test\Module\core; + +use SimpleSAML\Auth\Simple; +use SimpleSAML\Auth\AuthenticationFactory; +use SimpleSAML\Configuration; +use SimpleSAML\Error\Exception; +use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\Locale\Localization; +use SimpleSAML\Module\core\Controller; +use SimpleSAML\Session; +use SimpleSAML\Test\Utils\ClearStateTestCase; +use SimpleSAML\XHTML\Template; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; + +/** + * Set of tests for the controllers in the "core" module. + * + * For now, this test extends ClearStateTestCase so that it doesn't interfere with other tests. Once every class has + * been made PSR-7-aware, that won't be necessary any longer. + * + * @package SimpleSAML\Test + */ +class ControllerTest extends ClearStateTestCase +{ + + /** @var array */ + protected $authSources; + + /** @var \SimpleSAML\Configuration */ + protected $config; + + /** @var \SimpleSAML\Configuration[] */ + protected $loadedConfigs; + + /** @var \SimpleSAML\HTTP\Router */ + protected $router; + + + /** + * Set up for each test. + */ + protected function setUp() + { + parent::setUp(); + $this->authSources = [ + 'admin' => [ + 'core:adminPassword' + ], + 'example-userpass' => [ + 'exampleauth:UserPass', + 'username:password' => [ + 'uid' => ['test'] + ] + ] + ]; + $this->config = Configuration::loadFromArray( + [ + 'baseurlpath' => 'https://example.org/simplesaml', + 'module.enable' => ['exampleauth' => true], + 'usenewui' => true, + ], + '[ARRAY]', + 'simplesaml' + ); + Configuration::setPreLoadedConfig($this->config, 'config.php'); + } + + + /** + * Test that authentication is started immediately if we hit the login endpoint and there's only one non-admin + * source configured. + */ + public function testAutomaticLoginWhenOnlyOneSource() + { + $asConfig = Configuration::loadFromArray($this->authSources); + Configuration::setPreLoadedConfig($asConfig, 'authsources.php'); + $request = new Request(); + $session = Session::getSessionFromRequest(); + $factory = new AuthenticationFactory($this->config, $session); + $c = new Controller($this->config, $session, $factory); + $response = $c->login($request); + $this->assertInstanceOf(RunnableResponse::class, $response); + list($object, $method) = $response->getCallable(); + $this->assertInstanceOf(Simple::class, $object); + $this->assertEquals('login', $method); + $arguments = $response->getArguments(); + $this->assertArrayHasKey('ErrorURL', $arguments[0]); + $this->assertArrayHasKey('ReturnTo', $arguments[0]); + } + + + /** + * Test that the user can choose what auth source to use when there are multiple defined (admin excluded). + */ + public function testMultipleAuthSources() + { + $_SERVER['REQUEST_URI'] = '/'; + $asConfig = Configuration::loadFromArray( + array_merge( + $this->authSources, + [ + 'example-static' => [ + 'exampleauth:StaticSource', + 'uid' => ['test'] + ] + ] + ) + ); + Configuration::setPreLoadedConfig($asConfig, 'authsources.php'); + $request = new Request(); + $session = Session::getSessionFromRequest(); + $factory = new AuthenticationFactory($this->config, $session); + $c = new Controller($this->config, $session, $factory); + $response = $c->login($request); + $this->assertInstanceOf(Template::class, $response); + $this->assertEquals('core:login.twig', $response->getTemplateName()); + $this->assertArrayHasKey('sources', $response->data); + $this->assertArrayHasKey('example-userpass', $response->data['sources']); + $this->assertArrayHasKey('example-static', $response->data['sources']); + } + + + /** + * Test that specifying an invalid auth source while trying to login raises an exception. + */ + public function testLoginWithInvalidAuthSource() + { + $asConfig = Configuration::loadFromArray($this->authSources); + Configuration::setPreLoadedConfig($asConfig, 'authsources.php'); + $request = new Request(); + $session = Session::getSessionFromRequest(); + $factory = new AuthenticationFactory($this->config, $session); + $c = new Controller($this->config, $session, $factory); + $this->setExpectedException(Exception::class); + $c->login($request, 'invalid-auth-source'); + } + + + /** + * Test that we get redirected to /account/authsource when accessing the login endpoint while being already + * authenticated. + */ + public function testLoginWhenAlreadyAuthenticated() + { + $asConfig = Configuration::loadFromArray($this->authSources); + Configuration::setPreLoadedConfig($asConfig, 'authsources.php'); + $request = new Request(); + $session = Session::getSessionFromRequest(); + $session->setConfiguration($this->config); + $class = new \ReflectionClass($session); + $authData = $class->getProperty('authData'); + $authData->setAccessible(true); + $authData->setValue($session, [ + 'example-userpass' => [ + 'exampleauth:UserPass', + 'Attributes' => ['uid' => ['test']], + 'Authority' => 'example-userpass', + 'AuthnInstant' => time(), + 'Expire' => time() + 8 * 60* 60 + ] + ]); + $factory = new AuthenticationFactory($this->config, $session); + $c = new Controller($this->config, $session, $factory); + $response = $c->login($request); + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertEquals( + 'https://example.org/simplesaml/module.php/core/account/example-userpass', + $response->getTargetUrl() + ); + } + + + /** + * Test that triggering the logout controller actually proceeds to log out from the specified source. + */ + public function testLogout() + { + $asConfig = Configuration::loadFromArray($this->authSources); + Configuration::setPreLoadedConfig($asConfig, 'authsources.php'); + $session = Session::getSessionFromRequest(); + $factory = new AuthenticationFactory($this->config, $session); + $c = new Controller($this->config, $session, $factory); + $response = $c->logout('example-userpass'); + $this->assertInstanceOf(RunnableResponse::class, $response); + list($object, $method) = $response->getCallable(); + $this->assertInstanceOf(Simple::class, $object); + $this->assertEquals('logout', $method); + $this->assertEquals('/simplesaml/logout.php', $response->getArguments()[0]); + } + + + /** + * Test that accessing the "account" endpoint without being authenticated gets you redirected to the "login" + * endpoint. + */ + public function testNotAuthenticated() + { + $asConfig = Configuration::loadFromArray($this->authSources); + Configuration::setPreLoadedConfig($asConfig, 'authsources.php'); + $session = Session::getSessionFromRequest(); + $factory = new AuthenticationFactory($this->config, $session); + $c = new Controller($this->config, $session, $factory); + /** @var RedirectResponse $response */ + $response = $c->account('example-userpass'); + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertEquals( + 'https://example.org/simplesaml/module.php/core/login/example-userpass', + $response->getTargetUrl() + ); + } + + + /** + * Test that we are presented with a regular page if we are authenticated and try to access the "account" endpoint. + */ + public function testAuthenticated() + { + $asConfig = Configuration::loadFromArray($this->authSources); + Configuration::setPreLoadedConfig($asConfig, 'authsources.php'); + $session = Session::getSessionFromRequest(); + $class = new \ReflectionClass($session); + $authData = $class->getProperty('authData'); + $authData->setAccessible(true); + $authData->setValue($session, [ + 'example-userpass' => [ + 'exampleauth:UserPass', + 'Attributes' => ['uid' => ['test']], + 'Authority' => 'example-userpass', + 'AuthnInstant' => time(), + 'Expire' => time() + 8 * 60* 60 + ] + ]); + $factory = new AuthenticationFactory($this->config, $session); + $c = new Controller($this->config, $session, $factory); + /** @var \SimpleSAML\XHTML\Template $response */ + $response = $c->account('example-userpass'); + $this->assertInstanceOf(Template::class, $response); + $this->assertEquals('auth_status.twig', $response->getTemplateName()); + } +} diff --git a/tests/www/TemplateTest.php b/tests/www/TemplateTest.php index 06dfe10808c2ea612f0050eeb3c20a915fd99f72..2e9e3a4f4fc2d06e7e241136ba2e170dda5695f6 100644 --- a/tests/www/TemplateTest.php +++ b/tests/www/TemplateTest.php @@ -19,7 +19,7 @@ class TemplateTest extends TestCase public function testSyntax() { $config = Configuration::loadFromArray([ - 'language.i18n.backend' => 'gettext/gettext', + 'usenewui' => true, 'module.enable' => array_fill_keys(Module::getModules(), true), ]); Configuration::setPreLoadedConfig($config); diff --git a/www/index.php b/www/index.php index b5b456c7a8c3828829d28652896aae3aac3a1595..d080b8af96d8e7980c718fd48c73e72ef9c529f5 100644 --- a/www/index.php +++ b/www/index.php @@ -5,7 +5,7 @@ require_once('_include.php'); $config = \SimpleSAML\Configuration::getInstance(); if ($config->getBoolean('usenewui', false)) { - \SimpleSAML\Utils\HTTP::redirectTrustedURL(SimpleSAML\Module::getModuleURL('core/login.php')); + \SimpleSAML\Utils\HTTP::redirectTrustedURL(SimpleSAML\Module::getModuleURL('core/login')); } \SimpleSAML\Utils\HTTP::redirectTrustedURL(SimpleSAML\Module::getModuleURL('core/frontpage_welcome.php')); diff --git a/www/module.php b/www/module.php index caac395fb2ae60a16b314ffcdba0f5810e36c86d..83ee0ff382ea24cf6e1ec116e7d6b298d21a1eb0 100644 --- a/www/module.php +++ b/www/module.php @@ -1,171 +1,9 @@ <?php /** - * Handler for module requests. - * * This web page receives requests for web-pages hosted by modules, and directs them to - * the RequestHandler in the module. - * - * @author Olav Morken, UNINETT AS. - * @package SimpleSAMLphp + * the process() handler in the Module class. */ - require_once('_include.php'); -// index pages - file names to attempt when accessing directories -$indexFiles = ['index.php', 'index.html', 'index.htm', 'index.txt']; - -// MIME types - key is file extension, value is MIME type -$mimeTypes = [ - 'bmp' => 'image/x-ms-bmp', - 'css' => 'text/css', - 'gif' => 'image/gif', - 'htm' => 'text/html', - 'html' => 'text/html', - 'shtml' => 'text/html', - 'ico' => 'image/vnd.microsoft.icon', - 'jpe' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'js' => 'text/javascript', - 'pdf' => 'application/pdf', - 'png' => 'image/png', - 'svg' => 'image/svg+xml', - 'svgz' => 'image/svg+xml', - 'swf' => 'application/x-shockwave-flash', - 'swfl' => 'application/x-shockwave-flash', - 'txt' => 'text/plain', - 'xht' => 'application/xhtml+xml', - 'xhtml' => 'application/xhtml+xml', -]; - -if (empty($_SERVER['PATH_INFO'])) { - throw new \SimpleSAML\Error\NotFound('No PATH_INFO to module.php'); -} - -$url = $_SERVER['PATH_INFO']; -assert(substr($url, 0, 1) === '/'); - -/* clear the PATH_INFO option, so that a script can detect whether it is called with anything following the - *'.php'-ending. - */ -unset($_SERVER['PATH_INFO']); - -$modEnd = strpos($url, '/', 1); -if ($modEnd === false) { - // the path must always be on the form /module/ - throw new \SimpleSAML\Error\NotFound('The URL must at least contain a module name followed by a slash.'); -} - -$module = substr($url, 1, $modEnd - 1); -$url = substr($url, $modEnd + 1); -if ($url === false) { - $url = ''; -} - -if (!SimpleSAML\Module::isModuleEnabled($module)) { - throw new \SimpleSAML\Error\NotFound('The module \''.$module.'\' was either not found, or wasn\'t enabled.'); -} - -/* Make sure that the request isn't suspicious (contains references to current directory or parent directory or - * anything like that. Searching for './' in the URL will detect both '../' and './'. Searching for '\' will detect - * attempts to use Windows-style paths. - */ -if (strpos($url, '\\') !== false) { - throw new SimpleSAML\Error\BadRequest('Requested URL contained a backslash.'); -} elseif (strpos($url, './') !== false) { - throw new \SimpleSAML\Error\BadRequest('Requested URL contained \'./\'.'); -} - -$moduleDir = SimpleSAML\Module::getModuleDir($module).'/www/'; - -// check for '.php/' in the path, the presence of which indicates that another php-script should handle the request -for ($phpPos = strpos($url, '.php/'); $phpPos !== false; $phpPos = strpos($url, '.php/', $phpPos + 1)) { - $newURL = substr($url, 0, $phpPos + 4); - $param = substr($url, $phpPos + 4); - - if (is_file($moduleDir.$newURL)) { - /* $newPath points to a normal file. Point execution to that file, and - * save the remainder of the path in PATH_INFO. - */ - $url = $newURL; - $_SERVER['PATH_INFO'] = $param; - break; - } -} - -$path = $moduleDir.$url; - -if ($path[strlen($path) - 1] === '/') { - // path ends with a slash - directory reference. Attempt to find index file in directory - foreach ($indexFiles as $if) { - if (file_exists($path.$if)) { - $path .= $if; - break; - } - } -} - -if (is_dir($path)) { - /* Path is a directory - maybe no index file was found in the previous step, or maybe the path didn't end with - * a slash. Either way, we don't do directory listings. - */ - throw new \SimpleSAML\Error\NotFound('Directory listing not available.'); -} - -if (!file_exists($path)) { - // file not found - SimpleSAML\Logger::info('Could not find file \''.$path.'\'.'); - throw new \SimpleSAML\Error\NotFound('The URL wasn\'t found in the module.'); -} - -if (preg_match('#\.php$#D', $path)) { - // PHP file - attempt to run it - - /* In some environments, $_SERVER['SCRIPT_NAME'] is already set with $_SERVER['PATH_INFO']. Check for that case, - * and append script name only if necessary. - * - * Contributed by Travis Hegner. - */ - $script = "/$module/$url"; - if (stripos($_SERVER['SCRIPT_NAME'], $script) === false) { - $_SERVER['SCRIPT_NAME'] .= '/'.$module.'/'.$url; - } - - require($path); - exit(); -} - -// some other file type - attempt to serve it - -// find MIME type for file, based on extension -$contentType = null; -if (preg_match('#\.([^/\.]+)$#D', $path, $type)) { - $type = strtolower($type[1]); - if (array_key_exists($type, $mimeTypes)) { - $contentType = $mimeTypes[$type]; - } -} - -if ($contentType === null) { - /* We were unable to determine the MIME type from the file extension. Fall back to mime_content_type (if it - * exists). - */ - if (function_exists('mime_content_type')) { - $contentType = mime_content_type($path); - } else { - // mime_content_type doesn't exist. Return a default MIME type - SimpleSAML\Logger::warning('Unable to determine mime content type of file: '.$path); - $contentType = 'application/octet-stream'; - } -} - -$contentLength = sprintf('%u', filesize($path)); // force filesize to an unsigned number - -header('Content-Type: '.$contentType); -header('Content-Length: '.$contentLength); -header('Cache-Control: public,max-age=86400'); -header('Expires: '.gmdate('D, j M Y H:i:s \G\M\T', time() + 10 * 60)); -header('Last-Modified: '.gmdate('D, j M Y H:i:s \G\M\T', filemtime($path))); - -readfile($path); +\SimpleSAML\Module::process()->send();