diff --git a/actions-services.yml b/actions-services.yml index 48a1d348..0fc10d60 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -5,8 +5,11 @@ services: depends_on: - ssp-hub.local - ssp-idp1.local + - ssp-idp2.local - ssp-sp1.local - test-browser + environment: + - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub volumes: - ./dockerbuild/run-integration-tests.sh:/data/run-integration-tests.sh - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -67,6 +70,9 @@ services: # Misc. files needed - ./development/enable-exampleauth-module.sh:/data/enable-exampleauth-module.sh + # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code + - ./development/idp-local/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' @@ -75,6 +81,37 @@ services: ADMIN_PASS: "a" SECRET_SALT: "not-secret-h57fjemb&dn^nsJFGNjweJ" IDP_NAME: "IDP 1" + PROFILE_URL: "http://ssp-hub-sp1:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL_FOR_TESTS: "http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub" + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + THEME_USE: "default" + + ssp-idp2.local: + build: . + volumes: + # Utilize custom certs + - ./development/idp2-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/idp2-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + - ./development/idp2-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + + # Utilize custom metadata + - ./development/idp2-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php + - ./development/idp2-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + + # Local modules + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + command: /data/run.sh + ports: + - "8086:80" + environment: + ADMIN_EMAIL: "john_doe@there.com" + ADMIN_PASS: "b" + SECRET_SALT: "h57fjemb&dn^nsJFGNjweJ" + IDP_NAME: "IDP 2" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" THEME_USE: "material:material" diff --git a/behat.yml b/behat.yml index 9c205e35..053d4d3c 100644 --- a/behat.yml +++ b/behat.yml @@ -14,4 +14,4 @@ default: contexts: [ 'FeatureContext' ] profilereview_features: paths: [ '%paths.base%//features//profilereview.feature' ] - contexts: [ 'FeatureContext' ] + contexts: [ 'ProfileReviewContext' ] diff --git a/composer.json b/composer.json index da75894e..fa3b6c49 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,11 @@ "simplesamlphp/composer-module-installer": "1.1.8", "silinternational/simplesamlphp-module-silauth": "^7.1.1", "silinternational/simplesamlphp-module-mfa": "^5.2.1", - "silinternational/simplesamlphp-module-profilereview": "^2.1.0", "silinternational/ssp-utilities": "^1.1.0", "silinternational/simplesamlphp-module-material": "^8.1.1", "silinternational/simplesamlphp-module-sildisco": "^4.0.0", "silinternational/php-env": "^3.1.0", + "silinternational/psr3-adapters": "^3.1", "gettext/gettext": "^4.8@dev" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 1eb2f63e..2ac04f9b 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": "aa7c6c86f5fefed69d818d69b1a506c3", + "content-hash": "b4f4532a5a284c0b780aaa48860974ae", "packages": [ { "name": "aws/aws-crt-php", @@ -3038,59 +3038,6 @@ }, "time": "2023-06-15T13:38:51+00:00" }, - { - "name": "silinternational/simplesamlphp-module-profilereview", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/silinternational/simplesamlphp-module-profilereview.git", - "reference": "4c1df2eddcd50147aec198128446c6875c751616" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silinternational/simplesamlphp-module-profilereview/zipball/4c1df2eddcd50147aec198128446c6875c751616", - "reference": "4c1df2eddcd50147aec198128446c6875c751616", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.2", - "silinternational/php-env": "^2.1 || ^3.0", - "silinternational/psr3-adapters": "^1.1 || ^2.0 || ^3.0", - "simplesamlphp/simplesamlphp": "~1.18.6 || ~1.19.0", - "sinergi/browser-detector": "^6.1" - }, - "require-dev": { - "behat/behat": "^3.3", - "behat/mink": "^1.7", - "behat/mink-goutte-driver": "^1.2", - "phpunit/phpunit": "^8.4", - "roave/security-advisories": "dev-master" - }, - "type": "simplesamlphp-module", - "autoload": { - "psr-4": { - "Sil\\SspProfileReview\\": "src/", - "Sil\\SspProfileReview\\Behat\\": "features/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1-or-later" - ], - "authors": [ - { - "name": "Matt Henderson", - "email": "matt_henderson@sil.org" - } - ], - "description": "A simpleSAMLphp module for prompting the user to review their profile (such as 2fa, email, etc.).", - "support": { - "issues": "https://github.com/silinternational/simplesamlphp-module-profilereview/issues", - "source": "https://github.com/silinternational/simplesamlphp-module-profilereview/tree/2.1.0" - }, - "time": "2022-09-28T13:50:19+00:00" - }, { "name": "silinternational/simplesamlphp-module-silauth", "version": "7.1.1", diff --git a/development/hub/metadata/idp-remote.php b/development/hub/metadata/idp-remote.php index a729f5e4..2b324bf4 100644 --- a/development/hub/metadata/idp-remote.php +++ b/development/hub/metadata/idp-remote.php @@ -57,7 +57,7 @@ ], 'IDPNamespace' => 'IDP-2-custom-port', 'logoCaption' => 'IDP-2:8086 staff', - 'enabled' => false, + 'enabled' => true, 'betaEnabled' => true, 'logoURL' => 'https://dummyimage.com/125x125/0f4fbd/ffffff.png&text=IDP+2+8086', @@ -75,7 +75,7 @@ ], 'IDPNamespace' => 'IDP-2', 'logoCaption' => 'IDP-2 staff', - 'enabled' => false, + 'enabled' => true, 'betaEnabled' => true, 'logoURL' => 'https://dummyimage.com/125x125/0f4fbd/ffffff.png&text=IDP+2', diff --git a/development/idp-local/UserPass.php b/development/idp-local/UserPass.php new file mode 100644 index 00000000..ebb331d0 --- /dev/null +++ b/development/idp-local/UserPass.php @@ -0,0 +1,91 @@ +:", + * while the value of each element is a new array with the attributes for each user. + */ + private $users; + + /** + * Constructor for this authentication source. + * + * @param array $info Information about this authentication source. + * @param array $config Configuration. + */ + public function __construct($info, $config) + { + assert(is_array($info)); + assert(is_array($config)); + + // Call the parent constructor first, as required by the interface + parent::__construct($info, $config); + + $this->users = []; + + // Validate and parse our configuration + foreach ($config as $userpass => $attributes) { + if (!is_string($userpass)) { + throw new \Exception( + 'Invalid : for authentication source '.$this->authId.': '.$userpass + ); + } + + $userpass = explode(':', $userpass, 2); + if (count($userpass) !== 2) { + throw new \Exception( + 'Invalid : for authentication source '.$this->authId.': '.$userpass[0] + ); + } + $username = $userpass[0]; + $password = $userpass[1]; + +// try { +// $attributes = \SimpleSAML\Utils\Attributes::normalizeAttributesArray($attributes); +// } catch (\Exception $e) { +// throw new \Exception('Invalid attributes for user '.$username. +// ' in authentication source '.$this->authId.': '.$e->getMessage()); +// } + $this->users[$username.':'.$password] = $attributes; + } + } + + /** + * Attempt to log in using the given username and password. + * + * On a successful login, this function should return the users attributes. On failure, + * it should throw an exception. If the error was caused by the user entering the wrong + * username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown. + * + * Note that both the username and the password are UTF-8 encoded. + * + * @param string $username The username the user wrote. + * @param string $password The password the user wrote. + * @return array Associative array with the users attributes. + */ + protected function login($username, $password) + { + assert(is_string($username)); + assert(is_string($password)); + + $userpass = $username.':'.$password; + if (!array_key_exists($userpass, $this->users)) { + throw new \SimpleSAML\Error\Error('WRONGUSERPASS'); + } + + return $this->users[$userpass]; + } +} diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index c8b786ee..f9fa9ee5 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -65,5 +65,139 @@ 'invalid' ], ], + 'no_review:e' => [ + 'eduPersonPrincipalName' => ['NO_REVIEW@idp'], + 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], + 'sn' => ['Review'], + 'givenName' => ['No'], + 'mail' => ['no_review@example.com'], + 'employeeNumber' => ['11111'], + 'cn' => ['NO_REVIEW'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => 111, + 'type' => 'backupcode', + 'label' => '2SV #1', + 'created_utc' => '2017-10-24T20:40:47Z', + 'last_used_utc' => null, + 'data' => [ + 'count' => 10 + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + ], + 'profile_review' => 'no' + ], + 'mfa_add:f' => [ + 'eduPersonPrincipalName' => ['MFA_ADD@idp'], + 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], + 'sn' => ['Add'], + 'givenName' => ['Mfa'], + 'mail' => ['mfa_add@example.com'], + 'employeeNumber' => ['22222'], + 'cn' => ['MFA_ADD'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'yes', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + ], + 'profile_review' => 'no' + ], + 'method_add:g' => [ + 'eduPersonPrincipalName' => ['METHOD_ADD@methodidp'], + 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], + 'sn' => ['Add'], + 'givenName' => ['Method'], + 'mail' => ['method_add@example.com'], + 'employeeNumber' => ['44444'], + 'cn' => ['METHOD_ADD'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => 444, + 'type' => 'backupcode', + 'label' => '2SV #1', + 'created_utc' => '2017-10-24T20:40:47Z', + 'last_used_utc' => null, + 'data' => [ + 'count' => 10 + ], + ], + ], + ], + 'method' => [ + 'add' => 'yes', + ], + 'profile_review' => 'no' + ], + 'profile_review:h' => [ + 'eduPersonPrincipalName' => ['METHOD_REVIEW@methodidp'], + 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], + 'sn' => ['Review'], + 'givenName' => ['Method'], + 'mail' => ['method_review@example.com'], + 'employeeNumber' => ['55555'], + 'cn' => ['METHOD_REVIEW'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => 555, + 'type' => 'backupcode', + 'label' => '2SV #1', + 'created_utc' => '2017-10-24T20:40:47Z', + 'last_used_utc' => null, + 'data' => [ + 'count' => 10 + ], + ], + [ + 'id' => 556, + 'type' => 'manager', + 'label' => '2SV #2', + 'created_utc' => '2017-10-24T20:40:47Z', + 'last_used_utc' => '2017-10-24T20:41:57Z', + 'data' => [ + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [ + [ + 'id' => '55555555555555555555555555555555', + 'value' => 'method@example.com', + 'verified' => true, + 'created' => '2017-10-24T20:40:47Z', + ], + ], + ], + 'profile_review' => 'yes' + ], ], ]; diff --git a/development/idp-local/metadata/saml20-idp-hosted.php b/development/idp-local/metadata/saml20-idp-hosted.php index 55d541b4..588232b3 100644 --- a/development/idp-local/metadata/saml20-idp-hosted.php +++ b/development/idp-local/metadata/saml20-idp-hosted.php @@ -1,4 +1,8 @@ 'example-userpass', 'authproc' => [ - 10 => [ + 15 => [ 'class' => 'expirychecker:ExpiryDate', 'accountNameAttr' => 'cn', 'expiryDateAttr' => 'schacExpiryDate', @@ -37,8 +41,17 @@ 'dateFormat' => 'Y-m-d', 'loggerClass' => Psr3StdOutLogger::class, ], + 30 => [ + 'class' => 'profilereview:ProfileReview', + 'employeeIdAttr' => 'employeeNumber', + 'mfaLearnMoreUrl' => Env::get('MFA_LEARN_MORE_URL'), + 'profileUrl' => Env::get('PROFILE_URL'), + 'loggerClass' => Psr3SamlLogger::class, + ], ], ]; -// Duplicate configuration for port 80. +// Copy configuration for port 80 and modify host and profileUrl. $metadata['http://ssp-idp1.local'] = $metadata['http://ssp-idp1.local:8085']; +$metadata['http://ssp-idp1.local']['host'] = 'ssp-idp1.local'; +$metadata['http://ssp-idp1.local']['authproc'][30]['profileUrl'] = Env::get('PROFILE_URL_FOR_TESTS'); diff --git a/development/idp2-local/config/config.php b/development/idp2-local/config/config.php index ac0501e0..685f66f1 100644 --- a/development/idp2-local/config/config.php +++ b/development/idp2-local/config/config.php @@ -2,10 +2,15 @@ /* * The configuration of SimpleSAMLphp * + * 2020-04-17 -- Updated to simplesamlphp/config-templates/config.php 1.18.6 + * */ use Sil\PhpEnv\Env; use Sil\PhpEnv\EnvVarNotFoundException; -use Sil\SspUtils\AnnouncementUtils; + +/* + * Get config settings from ENV vars or set defaults + */ $logLevels = [ 'ERR' => SimpleSAML\Logger::ERR, // No statistics, only errors @@ -15,16 +20,13 @@ 'DEBUG' => SimpleSAML\Logger::DEBUG, // Full debug logs - not recommended for production ]; -/* - * Get config settings from ENV vars or set defaults - */ - try { // Required to be defined in environment variables $ADMIN_EMAIL = Env::requireEnv('ADMIN_EMAIL'); $ADMIN_PASS = Env::requireEnv('ADMIN_PASS'); $SECRET_SALT = Env::requireEnv('SECRET_SALT'); $IDP_NAME = Env::requireEnv('IDP_NAME'); + $IDP_DISPLAY_NAME = Env::get('IDP_DISPLAY_NAME', $IDP_NAME); } catch (EnvVarNotFoundException $e) { // Return error response code/message to HTTP request. @@ -49,57 +51,267 @@ $ENABLE_DEBUG = Env::get('ENABLE_DEBUG', false); $LOGGING_LEVEL = Env::get('LOGGING_LEVEL', 'NOTICE'); $LOGGING_HANDLER = Env::get('LOGGING_HANDLER', 'stderr'); -$SESSION_DURATION = (int)(Env::get('SESSION_DURATION', 540)); -$SESSION_DATASTORE_TIMEOUT = (int)(Env::get('SESSION_DATASTORE_TIMEOUT', (4 * 60 * 60))); // 4 hours -$SESSION_STATE_TIMEOUT = (int)(Env::get('SESSION_STATE_TIMEOUT', (60 * 60))); // 1 hour -$SESSION_COOKIE_LIFETIME = (int)(Env::get('SESSION_COOKIE_LIFETIME', 0)); -$SESSION_REMEMBERME_LIFETIME = (int)(Env::get('SESSION_REMEMBERME_LIFETIME', (14 * 86400))); // 14 days +$THEME_USE = Env::get('THEME_USE', 'material:material'); + +// Options: https://github.com/silinternational/simplesamlphp-module-material/blob/develop/README.md#branding +$THEME_COLOR_SCHEME = Env::get('THEME_COLOR_SCHEME', null); + $SECURE_COOKIE = Env::get('SECURE_COOKIE', true); -$THEME_USE = Env::get('THEME_USE', 'default'); +$SESSION_DURATION = (int)(Env::get('SESSION_DURATION', (60 * 60 * 10))); // 10 hours. +$SESSION_STORE_TYPE = Env::get('SESSION_STORE_TYPE', 'phpsession'); +$MEMCACHE_HOST1 = Env::get('MEMCACHE_HOST1', null); +$MEMCACHE_HOST2 = Env::get('MEMCACHE_HOST2', null); +$MEMCACHE_HOST1_PORT = Env::get('MEMCACHE_HOST1_PORT', 11211); +$MEMCACHE_HOST2_PORT = Env::get('MEMCACHE_HOST2_PORT', 11211); +$MYSQL_HOST = Env::get('MYSQL_HOST', ''); +$MYSQL_DATABASE = Env::get('MYSQL_DATABASE', ''); +$MYSQL_USER = Env::get('MYSQL_USER', ''); +$MYSQL_PASSWORD = Env::get('MYSQL_PASSWORD', ''); + $SAML20_IDP_ENABLE = Env::get('SAML20_IDP_ENABLE', true); $GOOGLE_ENABLE = Env::get('GOOGLE_ENABLE', false); +$HUB_MODE = Env::get('HUB_MODE', false); +$ANALYTICS_ID = Env::get('ANALYTICS_ID', null); +$PASSWORD_CHANGE_URL = Env::get('PASSWORD_CHANGE_URL'); +$PASSWORD_FORGOT_URL = Env::get('PASSWORD_FORGOT_URL'); +$HELP_CENTER_URL = Env::get('HELP_CENTER_URL'); $config = [ /* - * Get a string of html to show as an announcement on the discovery page - * and/or login page. By default, this will be fetched from - * .../vendor/simplesamlphp/simplesamlphp/announcement/announcement.php + * Whether this instance should act as a hub/proxy/bridge using sildisco + */ + 'hubmode' => $HUB_MODE, + + /* + * Name of this IdP + */ + 'idp_name' => $IDP_NAME, + + /* + * Name of this IdP to display to the user + */ + 'idp_display_name' => $IDP_DISPLAY_NAME, + + /* + * The tracking Id for Google Analytics or some other similar service */ - 'announcement' => AnnouncementUtils::getAnnouncement(), + 'analytics.trackingId' => $ANALYTICS_ID, + + 'passwordChangeUrl' => $PASSWORD_CHANGE_URL, + 'passwordForgotUrl' => $PASSWORD_FORGOT_URL, + 'helpCenterUrl' => $HELP_CENTER_URL, + + /******************************* + | BASIC CONFIGURATION OPTIONS | + *******************************/ /* - * Setup the following parameters to match the directory of your installation. + * Setup the following parameters to match your installation. * See the user manual for more details. - * - * Valid format for baseurlpath is: + */ + + /* + * baseurlpath is a *URL path* (not a filesystem path). + * A valid format for 'baseurlpath' is: * [(http|https)://(hostname|fqdn)[:port]]/[path/to/simplesaml/] - * (note that it must end with a '/') * - * The full url format is useful if your simpleSAMLphp setup is hosted behind + * The full url format is useful if your SimpleSAMLphp setup is hosted behind * a reverse proxy. In that case you can specify the external url here. * - * Please note that simpleSAMLphp will then redirect all queries to the + * Please note that SimpleSAMLphp will then redirect all queries to the * external url, no matter where you come from (direct access or via the * reverse proxy). */ 'baseurlpath' => $BASE_URL_PATH, + + /* + * The 'application' configuration array groups a set configuration options + * relative to an application protected by SimpleSAMLphp. + */ + //'application' => [ + /* + * The 'baseURL' configuration option allows you to specify a protocol, + * host and optionally a port that serves as the canonical base for all + * your application's URLs. This is useful when the environment + * observed in the server differs from the one observed by end users, + * for example, when using a load balancer to offload TLS. + * + * Note that this configuration option does not allow setting a path as + * part of the URL. If your setup involves URL rewriting or any other + * tricks that would result in SimpleSAMLphp observing a URL for your + * application's scripts different than the canonical one, you will + * need to compute the right URLs yourself and pass them dynamically + * to SimpleSAMLphp's API. + */ + //'baseURL' => 'https://example.com', + //], + + /* + * The following settings are *filesystem paths* which define where + * SimpleSAMLphp can find or write the following things: + * - 'certdir': The base directory for certificate and key material. + * - 'loggingdir': Where to write logs. + * - 'datadir': Storage of general data. + * - 'tempdir': Saving temporary files. SimpleSAMLphp will attempt to create + * this directory if it doesn't exist. + * When specified as a relative path, this is relative to the SimpleSAMLphp + * root directory. + */ 'certdir' => 'cert/', 'loggingdir' => 'log/', 'datadir' => 'data/', + 'tempdir' => '/tmp/simplesaml', + + /* + * Some information about the technical persons running this installation. + * The email address will be used as the recipient address for error reports, and + * also as the technical contact in generated metadata. + */ + 'technicalcontact_name' => $ADMIN_NAME, + 'technicalcontact_email' => $ADMIN_EMAIL, /* - * A directory where simpleSAMLphp can save temporary files. + * (Optional) The method by which email is delivered. Defaults to mail which utilizes the + * PHP mail() function. * - * SimpleSAMLphp will attempt to create this directory if it doesn't exist. + * Valid options are: mail, sendmail and smtp. */ - 'tempdir' => '/tmp/simplesaml', + //'mail.transport.method' => 'smtp', /* - * Name of this IdP to display to the user + * Set the transport options for the transport method specified. The valid settings are relative to the + * selected transport method. */ - 'idp_name' => $IDP_NAME, + // // smtp mail transport options + // 'mail.transport.options' => [ + // 'host' => 'mail.example.org', // required + // 'port' => 25, // optional + // 'username' => 'user@example.org', // optional: if set, enables smtp authentication + // 'password' => 'password', // optional: if set, enables smtp authentication + // 'security' => 'tls', // optional: defaults to no smtp security + // ], + // // sendmail mail transport options + // 'mail.transport.options' => [ + // 'path' => '/usr/sbin/sendmail' // optional: defaults to php.ini path + // ], + + /* + * The envelope from address for outgoing emails. + * This should be in a domain that has your application's IP addresses in its SPF record + * to prevent it from being rejected by mail filters. + */ + //'sendmail_from' => 'no-reply@example.org', + + /* + * The timezone of the server. This option should be set to the timezone you want + * SimpleSAMLphp to report the time in. The default is to guess the timezone based + * on your system timezone. + * + * See this page for a list of valid timezones: http://php.net/manual/en/timezones.php + */ + 'timezone' => $TIMEZONE, + + + + /********************************** + | SECURITY CONFIGURATION OPTIONS | + **********************************/ + + /* + * This is a secret salt used by SimpleSAMLphp when it needs to generate a secure hash + * of a value. It must be changed from its default value to a secret value. The value of + * 'secretsalt' can be any valid string of any length. + * + * A possible way to generate a random salt is by running the following command from a unix shell: + * LC_CTYPE=C tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo + */ + 'secretsalt' => $SECRET_SALT, + + /* + * This password must be kept secret, and modified from the default value 123. + * This password will give access to the installation page of SimpleSAMLphp with + * metadata listing and diagnostics pages. + * You can also put a hash here; run "bin/pwgen.php" to generate one. + */ + 'auth.adminpassword' => $ADMIN_PASS, + + /* + * Set this options to true if you want to require administrator password to access the web interface + * or the metadata pages, respectively. + */ + 'admin.protectindexpage' => $ADMIN_PROTECT_INDEX_PAGE, + 'admin.protectmetadata' => true, + + /* + * Set this option to false if you don't want SimpleSAMLphp to check for new stable releases when + * visiting the configuration tab in the web interface. + */ + 'admin.checkforupdates' => false, + + /* + * Array of domains that are allowed when generating links or redirects + * to URLs. SimpleSAMLphp will use this option to determine whether to + * to consider a given URL valid or not, but you should always validate + * URLs obtained from the input on your own (i.e. ReturnTo or RelayState + * parameters obtained from the $_REQUEST array). + * + * SimpleSAMLphp will automatically add your own domain (either by checking + * it dynamically, or by using the domain defined in the 'baseurlpath' + * directive, the latter having precedence) to the list of trusted domains, + * in case this option is NOT set to NULL. In that case, you are explicitly + * telling SimpleSAMLphp to verify URLs. + * + * Set to an empty array to disallow ALL redirects or links pointing to + * an external URL other than your own domain. This is the default behaviour. + * + * Set to NULL to disable checking of URLs. DO NOT DO THIS UNLESS YOU KNOW + * WHAT YOU ARE DOING! + * + * Example: + * 'trusted.url.domains' => ['sp.example.com', 'app.example.com'], + */ + 'trusted.url.domains' => null, + + /* + * Enable regular expression matching of trusted.url.domains. + * + * Set to true to treat the values in trusted.url.domains as regular + * expressions. Set to false to do exact string matching. + * + * If enabled, the start and end delimiters ('^' and '$') will be added to + * all regular expressions in trusted.url.domains. + */ + 'trusted.url.regex' => false, + + /* + * Enable secure POST from HTTPS to HTTP. + * + * If you have some SP's on HTTP and IdP is normally on HTTPS, this option + * enables secure POSTing to HTTP endpoint without warning from browser. + * + * For this to work, module.php/core/postredirect.php must be accessible + * also via HTTP on IdP, e.g. if your IdP is on + * https://idp.example.org/ssp/, then + * http://idp.example.org/ssp/module.php/core/postredirect.php must be accessible. + */ + 'enable.http_post' => false, + + /* + * Set the allowed clock skew between encrypting/decrypting assertions + * + * If you have an server that is constantly out of sync, this option + * allows you to adjust the allowed clock-skew. + * + * Allowed range: 180 - 300 + * Defaults to 180. + */ + 'assertion.allowed_clock_skew' => 180, + + /************************ + | ERRORS AND DEBUGGING | + ************************/ /* * The 'debug' option allows you to control how SimpleSAMLphp behaves in certain @@ -140,75 +352,36 @@ ], /* - * When showerrors is enabled, all error messages and stack traces will be output + * When 'showerrors' is enabled, all error messages and stack traces will be output * to the browser. * - * When errorreporting is enabled, a form will be presented for the user to report - * the error to technicalcontact_email. + * When 'errorreporting' is enabled, a form will be presented for the user to report + * the error to 'technicalcontact_email'. */ 'showerrors' => $SHOW_SAML_ERRORS, 'errorreporting' => false, /* - * Custom error show function called from SimpleSAML_Error_Error::show. + * Custom error show function called from SimpleSAML\Error\Error::show. * See docs/simplesamlphp-errorhandling.txt for function code example. * * Example: - * 'errors.show_function' => array('sspmod_example_Error_Show', 'show'), - */ - - /* - * This option allows you to enable validation of XML data against its - * schemas. A warning will be written to the log if validation fails. - */ - 'debug.validatexml' => false, - - /* - * This password must be kept secret, and modified from the default value 123. - * This password will give access to the installation page of simpleSAMLphp with - * metadata listing and diagnostics pages. - * You can also put a hash here; run "bin/pwgen.php" to generate one. + * 'errors.show_function' => ['SimpleSAML\Module\example\Error', 'show'], */ - 'auth.adminpassword' => $ADMIN_PASS, - 'admin.protectindexpage' => $ADMIN_PROTECT_INDEX_PAGE, - 'admin.protectmetadata' => true, - /* - * This is a secret salt used by simpleSAMLphp when it needs to generate a secure hash - * of a value. It must be changed from its default value to a secret value. The value of - * 'secretsalt' can be any valid string of any length. - * - * A possible way to generate a random salt is by running the following command from a unix shell: - * tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo - */ - 'secretsalt' => $SECRET_SALT, - /* - * Some information about the technical persons running this installation. - * The email address will be used as the recipient address for error reports, and - * also as the technical contact in generated metadata. - */ - 'technicalcontact_name' => $ADMIN_NAME, - 'technicalcontact_email' => $ADMIN_EMAIL, - /* - * The timezone of the server. This option should be set to the timezone you want - * simpleSAMLphp to report the time in. The default is to guess the timezone based - * on your system timezone. - * - * See this page for a list of valid timezones: http://php.net/manual/en/timezones.php - */ - 'timezone' => $TIMEZONE, + /************************** + | LOGGING AND STATISTICS | + **************************/ /* - * Logging. - * - * define the minimum log level to log - * SimpleSAML\Logger::ERR No statistics, only errors - * SimpleSAML\Logger::WARNING No statistics, only warnings/errors - * SimpleSAML\Logger::NOTICE Statistics and errors - * SimpleSAML\Logger::INFO Verbose logs - * SimpleSAML\Logger::DEBUG Full debug logs - not reccomended for production + * Define the minimum log level to log. Available levels: + * - SimpleSAML\Logger::ERR No statistics, only errors + * - SimpleSAML\Logger::WARNING No statistics, only warnings/errors + * - SimpleSAML\Logger::NOTICE Statistics and errors + * - SimpleSAML\Logger::INFO Verbose logs + * - SimpleSAML\Logger::DEBUG Full debug logs - not recommended for production * * Choose logging handler. * @@ -249,7 +422,7 @@ /* * Choose which facility should be used when logging with syslog. * - * These can be used for filtering the syslog output from simpleSAMLphp into its + * These can be used for filtering the syslog output from SimpleSAMLphp into its * own file by configuring the syslog daemon. * * See the documentation for openlog (http://php.net/manual/en/function.openlog.php) for available @@ -265,12 +438,12 @@ */ 'logging.processname' => 'simplesamlphp', - /* Logging: file - Logfilename in the loggingdir from above. + /* + * Logging: file - Logfilename in the loggingdir from above. */ 'logging.logfile' => 'simplesamlphp.log', - /* (New) statistics output configuration. - * + /* * This is an array of outputs. Each output has at least a 'class' option, which * selects the output. */ @@ -291,35 +464,129 @@ ], + + /*********************** + | PROXY CONFIGURATION | + ***********************/ + /* - * Enable + * Proxy to use for retrieving URLs. * - * Which functionality in simpleSAMLphp do you want to enable. Normally you would enable only + * Example: + * 'proxy' => 'tcp://proxy.example.com:5100' + */ + 'proxy' => null, + + /* + * Username/password authentication to proxy (Proxy-Authorization: Basic) + * Example: + * 'proxy.auth' = 'myuser:password' + */ + //'proxy.auth' => 'myuser:password', + + + + /************************** + | DATABASE CONFIGURATION | + **************************/ + + /* + * This database configuration is optional. If you are not using + * core functionality or modules that require a database, you can + * skip this configuration. + */ + + /* + * Database connection string. + * Ensure that you have the required PDO database driver installed + * for your connection string. + */ + //'database.dsn' => 'mysql:host=localhost;dbname=saml', + + /* + * SQL database credentials + */ + //'database.username' => 'simplesamlphp', + //'database.password' => 'secret', + //'database.options' => [], + + /* + * (Optional) Table prefix + */ + //'database.prefix' => '', + + /* + * (Optional) Driver options + */ + //'database.driver_options' => [], + + /* + * True or false if you would like a persistent database connection + */ + //'database.persistent' => false, + + /* + * Database slave configuration is optional as well. If you are only + * running a single database server, leave this blank. If you have + * a master/slave configuration, you can define as many slave servers + * as you want here. Slaves will be picked at random to be queried from. + * + * Configuration options in the slave array are exactly the same as the + * options for the master (shown above) with the exception of the table + * prefix and driver options. + */ + //'database.slaves' => [ + // /* + // [ + // 'dsn' => 'mysql:host=myslave;dbname=saml', + // 'username' => 'simplesamlphp', + // 'password' => 'secret', + // 'persistent' => false, + // ], + // */ + //], + + + + /************* + | PROTOCOLS | + *************/ + + /* + * Which functionality in SimpleSAMLphp do you want to enable. Normally you would enable only * one of the functionalities below, but in some cases you could run multiple functionalities. * In example when you are setting up a federation bridge. */ 'enable.saml20-idp' => $SAML20_IDP_ENABLE, 'enable.shib13-idp' => false, 'enable.adfs-idp' => false, - 'enable.wsfed-sp' => false, - 'enable.authmemcookie' => false, - /* - * Module enable configuration + * Whether SimpleSAMLphp should sign the response or the assertion in SAML 1.1 authentication + * responses. * + * The default is to sign the assertion element, but that can be overridden by setting this + * option to TRUE. It can also be overridden on a pr. SP basis by adding an option with the + * same name to the metadata of the SP. + */ + 'shib13.signresponse' => true, + + + + /*********** + | MODULES | + ***********/ + + /* * Configuration to override module enabling/disabling. * * Example: * - * 'module.enable' => array( - * // Setting to TRUE enables. - * 'exampleauth' => TRUE, - * // Setting to FALSE disables. - * 'saml' => FALSE, - * // Unset or NULL uses default. - * 'core' => NULL, - * ), + * 'module.enable' => [ + * 'exampleauth' => true, // Setting to TRUE enables. + * 'consent' => false, // Setting to FALSE disables. + * 'core' => null, // Unset or NULL uses default. + * ], * */ @@ -334,6 +601,12 @@ 'sildisco' => true, ], + + + /************************* + | SESSION CONFIGURATION | + *************************/ + /* * This value is the duration of the session in seconds. Make sure that the time duration of * cookies both at the SP and the IdP exceeds this duration. @@ -341,16 +614,16 @@ 'session.duration' => $SESSION_DURATION, /* - * Sets the duration, in seconds, data should be stored in the datastore. As the datastore is used for - * login and logout requests, thid option will control the maximum time these operations can take. + * Sets the duration, in seconds, data should be stored in the datastore. As the data store is used for + * login and logout requests, this option will control the maximum time these operations can take. * The default is 4 hours (4*60*60) seconds, which should be more than enough for these operations. */ - 'session.datastore.timeout' => $SESSION_DATASTORE_TIMEOUT, + 'session.datastore.timeout' => $SESSION_DURATION, /* * Sets the duration, in seconds, auth state should be stored. */ - 'session.state.timeout' => $SESSION_STATE_TIMEOUT, + 'session.state.timeout' => $SESSION_DURATION, /* * Option to override the default settings for the session cookie name @@ -365,7 +638,7 @@ * Example: * 'session.cookie.lifetime' => 30*60, */ - 'session.cookie.lifetime' => $SESSION_COOKIE_LIFETIME, + 'session.cookie.lifetime' => 0, /* * Limit the path of the cookies. @@ -397,28 +670,21 @@ 'session.cookie.secure' => $SECURE_COOKIE, /* - * When set to FALSE fallback to transient session on session initialization - * failure, throw exception otherwise. - */ - 'session.disable_fallback' => false, - - /* - * Enable secure POST from HTTPS to HTTP. + * Set the SameSite attribute in the cookie. * - * If you have some SP's on HTTP and IdP is normally on HTTPS, this option - * enables secure POSTing to HTTP endpoint without warning from browser. + * You can set this to the strings 'None', 'Lax', or 'Strict' to support + * the RFC6265bis SameSite cookie attribute. If set to null, no SameSite + * attribute will be sent. * - * For this to work, module.php/core/postredirect.php must be accessible - * also via HTTP on IdP, e.g. if your IdP is on - * https://idp.example.org/ssp/, then - * http://idp.example.org/ssp/module.php/core/postredirect.php must be accessible. + * Example: + * 'session.cookie.samesite' => 'None', */ - 'enable.http_post' => false, + 'session.cookie.samesite' => null, /* * Options to override the default settings for php sessions. */ - 'session.phpsession.cookiename' => null, + 'session.phpsession.cookiename' => 'SimpleSAML', 'session.phpsession.savepath' => null, 'session.phpsession.httponly' => true, @@ -442,23 +708,195 @@ */ 'session.rememberme.enable' => false, 'session.rememberme.checked' => false, - 'session.rememberme.lifetime' => $SESSION_REMEMBERME_LIFETIME, + 'session.rememberme.lifetime' => (14 * 86400), // 14 days - /** + /* * Custom function for session checking called on session init and loading. * See docs/simplesamlphp-advancedfeatures.txt for function code example. * * Example: - * 'session.check_function' => array('sspmod_example_Util', 'checkSession'), + * 'session.check_function' => ['\SimpleSAML\Module\example\Util', 'checkSession'], */ + + + /************************** + | MEMCACHE CONFIGURATION | + **************************/ + /* - * Languages available, RTL languages, and what language is default + * Configuration for the 'memcache' session store. This allows you to store + * multiple redundant copies of sessions on different memcache servers. + * + * 'memcache_store.servers' is an array of server groups. Every data + * item will be mirrored in every server group. + * + * Each server group is an array of servers. The data items will be + * load-balanced between all servers in each server group. + * + * Each server is an array of parameters for the server. The following + * options are available: + * - 'hostname': This is the hostname or ip address where the + * memcache server runs. This is the only required option. + * - 'port': This is the port number of the memcache server. If this + * option isn't set, then we will use the 'memcache.default_port' + * ini setting. This is 11211 by default. + * + * When using the "memcache" extension, the following options are also + * supported: + * - 'weight': This sets the weight of this server in this server + * group. http://php.net/manual/en/function.Memcache-addServer.php + * contains more information about the weight option. + * - 'timeout': The timeout for this server. By default, the timeout + * is 3 seconds. + * + * Example of redundant configuration with load balancing: + * This configuration makes it possible to lose both servers in the + * a-group or both servers in the b-group without losing any sessions. + * Note that sessions will be lost if one server is lost from both the + * a-group and the b-group. + * + * 'memcache_store.servers' => [ + * [ + * ['hostname' => 'mc_a1'], + * ['hostname' => 'mc_a2'], + * ], + * [ + * ['hostname' => 'mc_b1'], + * ['hostname' => 'mc_b2'], + * ], + * ], + * + * Example of simple configuration with only one memcache server, + * running on the same computer as the web server: + * Note that all sessions will be lost if the memcache server crashes. + * + * 'memcache_store.servers' => [ + * [ + * ['hostname' => 'localhost'], + * ], + * ], + * + * Additionally, when using the "memcached" extension, unique keys must + * be provided for each group of servers if persistent connections are + * desired. Each server group can also have an "options" indexed array + * with the options desired for the given group: + * + * 'memcache_store.servers' => [ + * 'memcache_group_1' => [ + * 'options' => [ + * \Memcached::OPT_BINARY_PROTOCOL => true, + * \Memcached::OPT_NO_BLOCK => true, + * \Memcached::OPT_TCP_NODELAY => true, + * \Memcached::OPT_LIBKETAMA_COMPATIBLE => true, + * ], + * ['hostname' => '127.0.0.1', 'port' => 11211], + * ['hostname' => '127.0.0.2', 'port' => 11211], + * ], + * + * 'memcache_group_2' => [ + * 'options' => [ + * \Memcached::OPT_BINARY_PROTOCOL => true, + * \Memcached::OPT_NO_BLOCK => true, + * \Memcached::OPT_TCP_NODELAY => true, + * \Memcached::OPT_LIBKETAMA_COMPATIBLE => true, + * ], + * ['hostname' => '127.0.0.3', 'port' => 11211], + * ['hostname' => '127.0.0.4', 'port' => 11211], + * ], + * ], + * + */ + 'memcache_store.servers' => [ + [ + [ + 'hostname' => $MEMCACHE_HOST1, + 'port' => $MEMCACHE_HOST1_PORT, + ], + ], + [ + [ + 'hostname' => $MEMCACHE_HOST2, + 'port' => $MEMCACHE_HOST2_PORT, + ], + ], + ], + + /* + * This value allows you to set a prefix for memcache-keys. The default + * for this value is 'simpleSAMLphp', which is fine in most cases. + * + * When running multiple instances of SSP on the same host, and more + * than one instance is using memcache, you probably want to assign + * a unique value per instance to this setting to avoid data collision. + */ + //'memcache_store.prefix' => '', + + /* + * This value is the duration data should be stored in memcache. Data + * will be dropped from the memcache servers when this time expires. + * The time will be reset every time the data is written to the + * memcache servers. + * + * This value should always be larger than the 'session.duration' + * option. Not doing this may result in the session being deleted from + * the memcache servers while it is still in use. + * + * Set this value to 0 if you don't want data to expire. + * + * Note: The oldest data will always be deleted if the memcache server + * runs out of storage space. + */ + 'memcache_store.expires' => $SESSION_DURATION + 3600, // Session duration plus an hour for clock skew + + + + /************************************* + | LANGUAGE AND INTERNATIONALIZATION | + *************************************/ + + /* + * Language-related options. + */ + 'language' => [ + /* + * An array in the form 'language' => . + * + * Each key in the array is the ISO 639 two-letter code for a language, + * and its value is an array with a list of alternative languages that + * can be used if the given language is not available at some point. + * Each alternative language is also specified by its ISO 639 code. + * + * For example, for the "no" language code (Norwegian), we would have: + * + * 'priorities' => [ + * 'no' => ['nb', 'nn', 'en', 'se'], + * ... + * ], + * + * establishing that if a translation for the "no" language code is + * not available, we look for translations in "nb" (Norwegian Bokmål), + * and so on, in that order. + */ + 'priorities' => [ + 'no' => ['nb', 'nn', 'en', 'se'], + 'nb' => ['no', 'nn', 'en', 'se'], + 'nn' => ['no', 'nb', 'en', 'se'], + 'se' => ['nb', 'no', 'nn', 'en'], + 'nr' => ['zu', 'en'], + 'nd' => ['zu', 'en'], + ], + ], + + /* + * Languages available, RTL languages, and what language is the default. */ - 'language.available' => array( - 'en', 'es', 'fr', 'pt', - ), - 'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'), + 'language.available' => [ + 'en', 'no', 'nn', 'se', 'da', 'de', 'sv', 'fi', 'es', 'ca', 'fr', 'it', 'nl', 'lb', + 'cs', 'sl', 'lt', 'hr', 'hu', 'pl', 'pt', 'pt-br', 'tr', 'ja', 'zh', 'zh-tw', 'ru', + 'et', 'he', 'id', 'sr', 'lv', 'ro', 'eu', 'el', 'af', 'zu', 'xh', + ], + 'language.rtl' => ['ar', 'dv', 'fa', 'ur', 'he'], 'language.default' => 'en', /* @@ -473,18 +911,21 @@ 'language.cookie.name' => 'language', 'language.cookie.domain' => null, 'language.cookie.path' => '/', + 'language.cookie.secure' => false, + 'language.cookie.httponly' => false, 'language.cookie.lifetime' => (60 * 60 * 24 * 900), + 'language.cookie.samesite' => null, /** - * Custom getLanguage function called from SimpleSAML_XHTML_Template::getLanguage(). + * Custom getLanguage function called from SimpleSAML\Locale\Language::getLanguage(). * Function should return language code of one of the available languages or NULL. - * See SimpleSAML_XHTML_Template::getLanguage() source code for more info. + * See SimpleSAML\Locale\Language::getLanguage() source code for more info. * * This option can be used to implement a custom function for determining * the default language for the user. * * Example: - * 'language.get_language_function' => array('sspmod_example_Template', 'getLanguage'), + * 'language.get_language_function' => ['\SimpleSAML\Module\example\Template', 'getLanguage'], */ /* @@ -513,16 +954,98 @@ */ 'attributes.extradictionary' => null, + + + /************** + | APPEARANCE | + **************/ + /* * Which theme directory should be used? */ 'theme.use' => $THEME_USE, + /* + * Set this option to the text you would like to appear at the header of each page. Set to false if you don't want + * any text to appear in the header. + */ + //'theme.header' => 'SimpleSAMLphp' + + /** + * A template controller, if any. + * + * Used to intercept certain parts of the template handling, while keeping away unwanted/unexpected hooks. Set + * the 'theme.controller' configuration option to a class that implements the + * \SimpleSAML\XHTML\TemplateControllerInterface interface to use it. + */ + //'theme.controller' => '', + + /* + * Templating options + * + * By default, twig templates are not cached. To turn on template caching: + * Set 'template.cache' to an absolute path pointing to a directory that + * SimpleSAMLphp has read and write permissions to. + */ + //'template.cache' => '', + + /* + * Set the 'template.auto_reload' to true if you would like SimpleSAMLphp to + * recompile the templates (when using the template cache) if the templates + * change. If you don't want to check the source templates for every request, + * set it to false. + */ + 'template.auto_reload' => false, + + /* + * Set this option to true to indicate that your installation of SimpleSAMLphp + * is running in a production environment. This will affect the way resources + * are used, offering an optimized version when running in production, and an + * easy-to-debug one when not. Set it to false when you are testing or + * developing the software, in which case a banner will be displayed to remind + * users that they're dealing with a non-production instance. + * + * Defaults to true. + */ + 'production' => true, + + /* + * SimpleSAMLphp modules can host static resources which are served through PHP. + * The serving of the resources can be configured through these settings. + */ + 'assets' => [ + /* + * These settings adjust the caching headers that are sent + * when serving static resources. + */ + 'caching' => [ + /* + * Amount of seconds before the resource should be fetched again + */ + 'max_age' => 86400, + /* + * Calculate a checksum of every file and send it to the browser + * This allows the browser to avoid downloading assets again in situations + * where the Last-Modified header cannot be trusted, + * for example in cluster setups + * + * Defaults false + */ + 'etag' => false, + ], + ], /* - * Default IdP for WS-Fed. + * If using the material theme, which color scheme to use + * Options: https://github.com/silinternational/simplesamlphp-module-material/blob/develop/README.md#branding */ - // 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost', + 'theme.color-scheme' => $THEME_COLOR_SCHEME, + + + + /********************* + | DISCOVERY SERVICE | + *********************/ /* * Whether the discovery service should allow the user to save his choice of IdP. @@ -530,7 +1053,9 @@ 'idpdisco.enableremember' => true, 'idpdisco.rememberchecked' => true, - // Disco service only accepts entities it knows. + /* + * The disco service only accepts entities it knows. + */ 'idpdisco.validate' => true, 'idpdisco.extDiscoveryStorage' => null, @@ -544,30 +1069,24 @@ * This makes it easier for the user to choose the IdP * * Options: [links,dropdown] - * */ 'idpdisco.layout' => 'links', - /* - * Whether simpleSAMLphp should sign the response or the assertion in SAML 1.1 authentication - * responses. - * - * The default is to sign the assertion element, but that can be overridden by setting this - * option to TRUE. It can also be overridden on a pr. SP basis by adding an option with the - * same name to the metadata of the SP. - */ - 'shib13.signresponse' => true, + /************************************* + | AUTHENTICATION PROCESSING FILTERS | + *************************************/ /* * Authentication processing filters that will be executed for all IdPs * Both Shibboleth and SAML 2.0 */ 'authproc.idp' => [ - /* Enable the authproc filter below to add URN Prefixces to all attributes - 10 => array( - 'class' => 'core:AttributeMap', 'addurnprefix' - ), */ + /* Enable the authproc filter below to add URN prefixes to all attributes + 10 => [ + 'class' => 'core:AttributeMap', 'addurnprefix' + ], + */ /* Enable the authproc filter below to automatically generated eduPersonTargetedID. 20 => 'core:TargetedID', */ @@ -575,89 +1094,133 @@ // Adopts language from attribute to use in UI 30 => 'core:LanguageAdaptor', - /* Add a realm attribute from edupersonprincipalname - 40 => 'core:AttributeRealm', - */ - 45 => [ + 35 => [ 'class' => 'core:StatisticsWithAttribute', 'attributename' => 'realm', 'type' => 'saml20-idp-SSO', ], + /* + * Copy friendly names attribute keys to oids ... + */ + 40 => [ + 'class' => 'core:AttributeMap', + 'name2oid', + '%duplicate', + ], + + /* + * Copy oid attribute keys to friendly names + */ + 41 => [ + 'class' => 'core:AttributeMap', + 'oid2name', + '%duplicate', + ], + + // 48 => *** WARNING: For Hubs this entry is added at the end of this file + + // If no attributes are requested in the SP metadata, then these will be sent through 50 => [ 'class' => 'core:AttributeLimit', 'default' => true, - 'eduPersonPrincipalName', 'sn', 'givenName', 'mail', + 'cn', + 'eduPersonPrincipalName', + 'eduPersonTargetID', + 'sn', + 'givenName', + 'mail', + 'employeeNumber', + 'urn:oid:2.5.4.3', // cn + 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', // eduPersonPrincipalName + 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10', // eduPersonTargetID + 'urn:oid:2.5.4.4', // sn + 'urn:oid:2.5.4.42', // givenName + 'urn:oid:0.9.2342.19200300.100.1.3', // mail + 'urn:oid:2.16.840.1.113730.3.1.3', // employeeNumber ], // Use the uid value to populate the nameid entry - 60 => [ - 'class' => 'saml:AttributeNameID', - 'attribute' => 'uid', - 'Format' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - ], + // 60 => [ + // 'class' => 'saml:AttributeNameID', + // 'attribute' => 'uid', + // 'Format' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + // ], /* * Search attribute "distinguishedName" for pattern and replaces if found - - 70 => array( + */ + /* + 70 => [ 'class' => 'core:AttributeAlter', 'pattern' => '/OU=studerende/', 'replacement' => 'Student', 'subject' => 'distinguishedName', '%replace', - ), - */ + ], + */ /* * Consent module is enabled (with no permanent storage, using cookies). - - 90 => array( + */ + /* + 90 => [ 'class' => 'consent:Consent', 'store' => 'consent:Cookie', 'focus' => 'yes', - 'checked' => TRUE - ), - */ - - + 'checked' => true + ], + */ // If language is set in Consent module it will be added as an attribute. 99 => 'core:LanguageAdaptor', ], + /* * Authentication processing filters that will be executed for all SPs * Both Shibboleth and SAML 2.0 */ 'authproc.sp' => [ /* - 10 => array( + 10 => [ 'class' => 'core:AttributeMap', 'removeurnprefix' - ), + ], */ /* * Generate the 'group' attribute populated from other variables, including eduPersonAffiliation. - 60 => array( + 60 => [ 'class' => 'core:GenerateGroups', 'eduPersonAffiliation' - ), + ], */ /* * All users will be members of 'users' and 'members' - 61 => array( - 'class' => 'core:AttributeAdd', 'groups' => array('users', 'members') - ), + */ + /* + 61 => [ + 'class' => 'core:AttributeAdd', 'groups' => ['users', 'members'] + ], */ // Adopts language from attribute to use in UI 90 => 'core:LanguageAdaptor', - ], + + /************************** + | METADATA CONFIGURATION | + **************************/ + + /* + * This option allows you to specify a directory for your metadata outside of the standard metadata directory + * included in the standard distribution of the software. + */ + //'metadatadir' => 'metadata', + /* * This option configures the metadata sources. The metadata sources is given as an array with - * different metadata sources. When searching for metadata, simpleSAMPphp will search through + * different metadata sources. When searching for metadata, SimpleSAMLphp will search through * the array from start to end. * * Each element in the array is an associative array which configures the metadata source. @@ -674,201 +1237,143 @@ * This metadata handler parses an XML file with either an EntityDescriptor element or an * EntitiesDescriptor element. The XML file may be stored locally, or (for debugging) on a remote * web server. - * The XML hetadata handler defines the following options: + * The XML metadata handler defines the following options: * - 'type': This is always 'xml'. * - 'file': Path to the XML file with the metadata. * - 'url': The URL to fetch metadata from. THIS IS ONLY FOR DEBUGGING - THERE IS NO CACHING OF THE RESPONSE. * + * MDQ metadata handler: + * This metadata handler looks up for the metadata of an entity at the given MDQ server. + * The MDQ metadata handler defines the following options: + * - 'type': This is always 'mdq'. + * - 'server': Base URL of the MDQ server. Mandatory. + * - 'validateFingerprint': The fingerprint of the certificate used to sign the metadata. You don't need this + * option if you don't want to validate the signature on the metadata. Optional. + * - 'cachedir': Directory where metadata can be cached. Optional. + * - 'cachelength': Maximum time metadata can be cached, in seconds. Defaults to 24 + * hours (86400 seconds). Optional. + * + * PDO metadata handler: + * This metadata handler looks up metadata of an entity stored in a database. + * + * Note: If you are using the PDO metadata handler, you must configure the database + * options in this configuration file. + * + * The PDO metadata handler defines the following options: + * - 'type': This is always 'pdo'. * * Examples: * * This example defines two flatfile sources. One is the default metadata directory, the other - * is a metadata directory with autogenerated metadata files. + * is a metadata directory with auto-generated metadata files. * - * 'metadata.sources' => array( - * array('type' => 'flatfile'), - * array('type' => 'flatfile', 'directory' => 'metadata-generated'), - * ), + * 'metadata.sources' => [ + * ['type' => 'flatfile'], + * ['type' => 'flatfile', 'directory' => 'metadata-generated'], + * ], * * This example defines a flatfile source and an XML source. - * 'metadata.sources' => array( - * array('type' => 'flatfile'), - * array('type' => 'xml', 'file' => 'idp.example.org-idpMeta.xml'), - * ), - * + * 'metadata.sources' => [ + * ['type' => 'flatfile'], + * ['type' => 'xml', 'file' => 'idp.example.org-idpMeta.xml'], + * ], + * + * This example defines an mdq source. + * 'metadata.sources' => [ + * [ + * 'type' => 'mdq', + * 'server' => 'http://mdq.server.com:8080', + * 'cachedir' => '/var/simplesamlphp/mdq-cache', + * 'cachelength' => 86400 + * ] + * ], + * + * This example defines an pdo source. + * 'metadata.sources' => [ + * ['type' => 'pdo'] + * ], * * Default: - * 'metadata.sources' => array( - * array('type' => 'flatfile') - * ), + * 'metadata.sources' => [ + * ['type' => 'flatfile'] + * ], */ 'metadata.sources' => [ ['type' => 'flatfile'], ], - /* - * Configure the datastore for simpleSAMLphp. - * - * - 'phpsession': Limited datastore, which uses the PHP session. - * - 'memcache': Key-value datastore, based on memcache. - * - 'sql': SQL datastore, using PDO. - * - * The default datastore is 'phpsession'. + * Should signing of generated metadata be enabled by default. * - * (This option replaces the old 'session.handler'-option.) + * Metadata signing can also be enabled for a individual SP or IdP by setting the + * same option in the metadata for the SP or IdP. */ - 'store.type' => 'phpsession', - + 'metadata.sign.enable' => true, /* - * The DSN the sql datastore should connect to. + * The default key & certificate which should be used to sign generated metadata. These + * are files stored in the cert dir. + * These values can be overridden by the options with the same names in the SP or + * IdP metadata. * - * See http://www.php.net/manual/en/pdo.drivers.php for the various - * syntaxes. - */ - 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3', - - /* - * The username and password to use when connecting to the database. + * If these aren't specified here or in the metadata for the SP or IdP, then + * the 'certificate' and 'privatekey' option in the metadata will be used. + * if those aren't set, signing of metadata will fail. */ - 'store.sql.username' => null, - 'store.sql.password' => null, + 'metadata.sign.privatekey' => 'saml.pem', + 'metadata.sign.privatekey_pass' => null, + 'metadata.sign.certificate' => 'saml.crt', - /* - * The prefix we should use on our tables. - */ - 'store.sql.prefix' => 'simpleSAMLphp', + /**************************** + | DATA STORE CONFIGURATION | + ****************************/ /* - * Configuration for the MemcacheStore class. This allows you to store - * multiple redudant copies of sessions on different memcache servers. - * - * 'memcache_store.servers' is an array of server groups. Every data - * item will be mirrored in every server group. - * - * Each server group is an array of servers. The data items will be - * load-balanced between all servers in each server group. - * - * Each server is an array of parameters for the server. The following - * options are available: - * - 'hostname': This is the hostname or ip address where the - * memcache server runs. This is the only required option. - * - 'port': This is the port number of the memcache server. If this - * option isn't set, then we will use the 'memcache.default_port' - * ini setting. This is 11211 by default. - * - 'weight': This sets the weight of this server in this server - * group. http://php.net/manual/en/function.Memcache-addServer.php - * contains more information about the weight option. - * - 'timeout': The timeout for this server. By default, the timeout - * is 3 seconds. + * Configure the data store for SimpleSAMLphp. * - * Example of redudant configuration with load balancing: - * This configuration makes it possible to lose both servers in the - * a-group or both servers in the b-group without losing any sessions. - * Note that sessions will be lost if one server is lost from both the - * a-group and the b-group. - * - * 'memcache_store.servers' => array( - * array( - * array('hostname' => 'mc_a1'), - * array('hostname' => 'mc_a2'), - * ), - * array( - * array('hostname' => 'mc_b1'), - * array('hostname' => 'mc_b2'), - * ), - * ), - * - * Example of simple configuration with only one memcache server, - * running on the same computer as the web server: - * Note that all sessions will be lost if the memcache server crashes. - * - * 'memcache_store.servers' => array( - * array( - * array('hostname' => 'localhost'), - * ), - * ), + * - 'phpsession': Limited datastore, which uses the PHP session. + * - 'memcache': Key-value datastore, based on memcache. + * - 'sql': SQL datastore, using PDO. + * - 'redis': Key-value datastore, based on redis. * + * The default datastore is 'phpsession'. */ - 'memcache_store.servers' => [ - [ - ['hostname' => 'localhost'], - ], - ], - + 'store.type' => $SESSION_STORE_TYPE, /* - * This value is the duration data should be stored in memcache. Data - * will be dropped from the memcache servers when this time expires. - * The time will be reset every time the data is written to the - * memcache servers. - * - * This value should always be larger than the 'session.duration' - * option. Not doing this may result in the session being deleted from - * the memcache servers while it is still in use. - * - * Set this value to 0 if you don't want data to expire. + * The DSN the sql datastore should connect to. * - * Note: The oldest data will always be deleted if the memcache server - * runs out of storage space. + * See http://www.php.net/manual/en/pdo.drivers.php for the various + * syntaxes. */ - 'memcache_store.expires' => 36 * (60 * 60), // 36 hours. - + 'store.sql.dsn' => sprintf('mysql:host=%s;dbname=%s', $MYSQL_HOST, $MYSQL_DATABASE), /* - * Should signing of generated metadata be enabled by default. - * - * Metadata signing can also be enabled for a individual SP or IdP by setting the - * same option in the metadata for the SP or IdP. + * The username and password to use when connecting to the database. */ - 'metadata.sign.enable' => true, + 'store.sql.username' => $MYSQL_USER, + 'store.sql.password' => $MYSQL_PASSWORD, /* - * The default key & certificate which should be used to sign generated metadata. These - * are files stored in the cert dir. - * These values can be overridden by the options with the same names in the SP or - * IdP metadata. - * - * If these aren't specified here or in the metadata for the SP or IdP, then - * the 'certificate' and 'privatekey' option in the metadata will be used. - * if those aren't set, signing of metadata will fail. + * The prefix we should use on our tables. */ - 'metadata.sign.privatekey' => 'ssp-hub.pem', - 'metadata.sign.privatekey_pass' => null, - 'metadata.sign.certificate' => 'ssp-hub.crt', - + //'store.sql.prefix' => 'SimpleSAMLphp', /* - * Proxy to use for retrieving URLs. - * - * Example: - * 'proxy' => 'tcp://proxy.example.com:5100' + * The hostname and port of the Redis datastore instance. */ - 'proxy' => null, + //'store.redis.host' => 'localhost', + //'store.redis.port' => 6379, /* - * Array of domains that are allowed when generating links or redirections - * to URLs. simpleSAMLphp will use this option to determine whether to - * to consider a given URL valid or not, but you should always validate - * URLs obtained from the input on your own (i.e. ReturnTo or RelayState - * parameters obtained from the $_REQUEST array). - * - * Set to NULL to disable checking of URLs. - * - * simpleSAMLphp will automatically add your own domain (either by checking - * it dinamically, or by using the domain defined in the 'baseurlpath' - * directive, the latter having precedence) to the list of trusted domains, - * in case this option is NOT set to NULL. In that case, you are explicitly - * telling simpleSAMLphp to verify URLs. - * - * Set to an empty array to disallow ALL redirections or links pointing to - * an external URL other than your own domain. - * - * Example: - * 'trusted.url.domains' => array('sp.example.com', 'app.example.com'), + * The prefix we should use on our Redis datastore. */ - 'trusted.url.domains' => null, - + //'store.redis.prefix' => 'SimpleSAMLphp', ]; + +if ($HUB_MODE) { + // prefix the 'member' (urn:oid:2.5.4.31) attribute elements with idp.idp_name. + $config['authproc.idp'][48] = 'sildisco:TagGroup'; + $config['authproc.idp'][49] = 'sildisco:AddIdp2NameId'; +} diff --git a/development/idp2-local/metadata/saml20-idp-hosted.php b/development/idp2-local/metadata/saml20-idp-hosted.php index bb380850..78ff4405 100644 --- a/development/idp2-local/metadata/saml20-idp-hosted.php +++ b/development/idp2-local/metadata/saml20-idp-hosted.php @@ -23,3 +23,7 @@ */ 'auth' => 'admin', ]; + +// Copy configuration for port 80 and modify host. +$metadata['http://ssp-idp2.local'] = $metadata['http://ssp-idp2.local:8086']; +$metadata['http://ssp-idp2.local']['host'] = 'ssp-idp2.local'; diff --git a/docker-compose.yml b/docker-compose.yml index 084d4391..cecf2a4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: # Local modules - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: ["/data/run-debug.sh"] ports: @@ -36,10 +37,12 @@ services: depends_on: - ssp-hub.local - ssp-idp1.local + - ssp-idp2.local - ssp-sp1.local - test-browser environment: - COMPOSER_CACHE_DIR=/composer + - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub volumes: - ./composer.json:/data/composer.json - ./composer.lock:/data/composer.lock @@ -52,6 +55,7 @@ services: - ./behat.yml:/data/behat.yml - ./tests:/data/tests - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: ["/data/run-tests.sh"] test-browser: @@ -98,6 +102,7 @@ services: # Local modules - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: /data/run-debug.sh ports: - "80:80" @@ -132,11 +137,15 @@ services: # Misc. files needed - ./development/enable-exampleauth-module.sh:/data/enable-exampleauth-module.sh + # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code + - ./development/idp-local/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' ports: - "8085:80" @@ -145,16 +154,18 @@ services: ADMIN_PASS: "a" SECRET_SALT: "h57fjemb&dn^nsJFGNjweJ" IDP_NAME: "IDP 1" + PROFILE_URL: "http://ssp-hub-sp1:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL_FOR_TESTS: "http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" - THEME_USE: "material:material" + THEME_USE: "default" SESSION_STORE_TYPE: "sql" MYSQL_HOST: "db" MYSQL_DATABASE: "silauth" MYSQL_USER: "silauth" MYSQL_PASSWORD: "silauth" - idp2: + ssp-idp2.local: build: . volumes: # Utilize custom certs @@ -170,6 +181,7 @@ services: # Local modules - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: /data/run.sh ports: - "8086:80" @@ -180,6 +192,7 @@ services: IDP_NAME: "IDP 2" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" + THEME_USE: "material:material" ssp-sp1.local: build: . @@ -199,6 +212,7 @@ services: # Local modules - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview ports: - "8081:80" environment: @@ -225,6 +239,7 @@ services: # Local modules - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview ports: - "8082:80" environment: diff --git a/features/bootstrap/ExpiryContext.php b/features/bootstrap/ExpiryContext.php index 68bc5d79..473ff18b 100644 --- a/features/bootstrap/ExpiryContext.php +++ b/features/bootstrap/ExpiryContext.php @@ -222,7 +222,7 @@ public function iProvideCredentialsThatHaveNoPasswordExpirationDate() public function iShouldSeeAnErrorMessage() { $page = $this->session->getPage(); - Assert::assertContains('An error occurred', $page->getHtml()); + Assert::assertContains('Unhandled exception', $page->getHtml()); } /** diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 93f39503..25cda925 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -15,7 +15,8 @@ class FeatureContext extends MinkContext private const HUB_DISCO_URL = 'http://ssp-hub.local/module.php/core/authenticate.php?as=hub-discovery'; private const HUB_HOME_URL = 'http://ssp-hub.local'; protected const SP1_LOGIN_PAGE = 'http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub'; - + protected const SP2_LOGIN_PAGE = 'http://ssp-sp2.local/module.php/core/authenticate.php?as=ssp-hub'; + /** @var Session */ protected $session; @@ -137,11 +138,18 @@ public function iClickOnTheTile($idpName) } /** - * @When I go to the SP1 login page + * @When I go to the :sp login page */ - public function iGoToTheSp1LoginPage() - { - $this->visit(self::SP1_LOGIN_PAGE); + public function iGoToTheSpLoginPage($sp) + { + switch ($sp) { + case 'SP1': + $this->visit(self::SP1_LOGIN_PAGE); + break; + case 'SP2': + $this->visit(self::SP2_LOGIN_PAGE); + break; + } } protected function assertPageBodyContainsText(string $expectedText) diff --git a/features/bootstrap/ProfileReviewContext.php b/features/bootstrap/ProfileReviewContext.php new file mode 100644 index 00000000..8e60c0d3 --- /dev/null +++ b/features/bootstrap/ProfileReviewContext.php @@ -0,0 +1,276 @@ +findAll('css', 'form'); + foreach ($forms as $form) { + if (strpos($form->getHtml(), $text) !== false) { + return; + } + } + Assert::fail(sprintf( + "No form found containing %s in this HTML:\n%s", + var_export($text, true), + $page->getHtml() + )); + } + + /** + * Get the login button from the given page. + * + * @param DocumentElement $page The page. + * @return NodeElement + */ + protected function getLoginButton($page) + { + $buttons = $page->findAll('css', 'button'); + $loginButton = null; + foreach ($buttons as $button) { + $lcButtonText = strtolower($button->getText()); + if (strpos($lcButtonText, 'login') !== false) { + $loginButton = $button; + break; + } + } + Assert::assertNotNull($loginButton, 'Failed to find the login button'); + return $loginButton; + } + + /** + * @When I login + */ + public function iLogin() + { + $page = $this->session->getPage(); + try { + $page->fillField('username', $this->username); + $page->fillField('password', $this->password); + $this->submitLoginForm($page); + } catch (ElementNotFoundException $e) { + Assert::fail(sprintf( + "Did not find that element in the page.\nError: %s\nPage content: %s", + $e->getMessage(), + $page->getContent() + )); + } + } + + /** + * @Then I should end up at my intended destination + */ + public function iShouldEndUpAtMyIntendedDestination() + { + $page = $this->session->getPage(); + Assert::assertContains('Your attributes', $page->getHtml()); + } + + /** + * Submit the current form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported) by + * clicking the specified button. + * + * @param string $buttonName The value of the desired button's `name` + * attribute. + */ + protected function submitFormByClickingButtonNamed($buttonName) + { + $page = $this->session->getPage(); + $button = $page->find('css', sprintf( + '[name=%s]', + $buttonName + )); + Assert::assertNotNull($button, 'Failed to find button named ' . $buttonName); + $button->click(); + $this->submitSecondarySspFormIfPresent($page); + } + + /** + * Submit the login form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitLoginForm($page) + { + $loginButton = $this->getLoginButton($page); + $loginButton->click(); + $this->submitSecondarySspFormIfPresent($page); + } + + /** + * Submit the secondary page's form (if simpleSAMLphp shows another page + * because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitSecondarySspFormIfPresent($page) + { + // SimpleSAMLphp 1.15 markup for secondary page: + $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); + if ($postLoginSubmitButton instanceof NodeElement) { + $postLoginSubmitButton->click(); + } else { + + // SimpleSAMLphp 1.14 markup for secondary page: + $body = $page->find('css', 'body'); + if ($body instanceof NodeElement) { + $onload = $body->getAttribute('onload'); + if ($onload === "document.getElementsByTagName('input')[0].click();") { + $body->pressButton('Submit'); + } + } + } + } + + /** + * @Given I provide credentials that do not need review + */ + public function iProvideCredentialsThatDoNotNeedReview() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'no_review'; + $this->password = 'e'; + } + + /** + * @Given I provide credentials that are due for a(n) :category :nagType reminder + */ + public function iProvideCredentialsThatAreDueForAReminder($category, $nagType) + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = $category . '_' . $nagType; + switch ($this->username) { + case 'mfa_add': + $this->password = 'f'; + break; + + case 'method_add': + $this->password = 'g'; + break; + + case 'profile_review': + $this->password = 'h'; + break; + } + } + + /** + * @Given I have logged in (again) + */ + public function iHaveLoggedIn() + { + $this->iLogin(); + } + + protected function pageContainsElementWithText($cssSelector, $text) + { + $page = $this->session->getPage(); + $elements = $page->findAll('css', $cssSelector); + foreach ($elements as $element) { + if (strpos($element->getText(), $text) !== false) { + return true; + } + } + return false; + } + + /** + * @Then there should be a way to continue to my intended destination + */ + public function thereShouldBeAWayToContinueToMyIntendedDestination() + { + $page = $this->session->getPage(); + $this->assertFormContains('name="continue"', $page); + } + + /** + * @When I click the remind-me-later button + */ + public function iClickTheRemindMeLaterButton() + { + $this->submitFormByClickingButtonNamed('continue'); + } + + /** + * @When I click the update profile button + */ + public function iClickTheUpdateProfileButton() + { + $this->submitFormByClickingButtonNamed('update'); + } + + /** + * @Then I should end up at the update profile URL + */ + public function iShouldEndUpAtTheUpdateProfileUrl() + { + $profileUrl = Env::get('PROFILE_URL_FOR_TESTS'); + Assert::assertNotEmpty($profileUrl, 'No PROFILE_URL_FOR_TESTS provided'); + $currentUrl = $this->session->getCurrentUrl(); + Assert::assertStringStartsWith( + $profileUrl, + $currentUrl, + 'Did NOT end up at the update profile URL' + ); + } + + /** + * @Then I should see the message: :message + */ + public function iShouldSeeTheMessage($message) + { + $page = $this->session->getPage(); + Assert::assertContains($message, $page->getHtml()); + } + + /** + * @Then there should be a way to go update my profile now + */ + public function thereShouldBeAWayToGoUpdateMyProfileNow() + { + $page = $this->session->getPage(); + $this->assertFormContains('name="update"', $page); + } + + /** + * @Given I provide credentials for a user that has used the manager mfa option + */ + public function iProvideCredentialsForAUserThatHasUsedTheManagerMfaOption() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'profile_review'; + $this->password = 'h'; + } + + /** + * @Then I should not see any manager mfa information + */ + public function iShouldNotSeeAnyManagerMfaInformation() + { + $page = $this->session->getPage(); + $isManagerMfaPresent = $page->hasContent('manager'); + Assert::assertFalse($isManagerMfaPresent, 'found manager mfa data'); + } +} diff --git a/features/material.feature b/features/material.feature index 9ed643bb..78da2688 100644 --- a/features/material.feature +++ b/features/material.feature @@ -21,8 +21,8 @@ Feature: Material theme Scenario: Login page When I go to the SP1 login page - And I click on the "IDP 1" tile - Then I should see a "Login with your IDP 1 identity" page + And I click on the "IDP 2" tile + Then I should see a "Login with your IDP 2 identity" page And I should see our material theme Scenario: Forgot password link diff --git a/features/profilereview.feature b/features/profilereview.feature index dbb843e5..56643e9d 100644 --- a/features/profilereview.feature +++ b/features/profilereview.feature @@ -1,7 +1,55 @@ -Feature: Profile review module - - Scenario: Nag user about having no MFA - - Scenario: Nag user about having no password recovery methods - - Scenario: Review user's profile +Feature: Prompt to review profile information + Background: + Given I go to the SP1 login page + And I click on the "IDP 1" tile + + Scenario: Don't ask for review + Given I provide credentials that do not need review + When I login + Then I should end up at my intended destination + + Scenario Outline: Present reminder as required by the user profile + Given I provide credentials that are due for a reminder + When I login + Then I should see the message: + And there should be a way to go update my profile now + And there should be a way to continue to my intended destination + + Examples: + | category | nag type | message | + | mfa | add | "2-Step Verification" | + | method | add | "alternate email addresses" | + | profile | review | "Please take a moment to review" | + + Scenario Outline: Obeying a reminder + Given I provide credentials that are due for a reminder + And I have logged in + When I click the update profile button + # FIXME: It is currently required to login again, but it shouldn't be necessary. + And I click on the "IDP 1" tile + And I login + Then I should end up at the update profile URL + + Examples: + | category | nag type | + | mfa | add | + | method | add | + | profile | review | + + Scenario Outline: Ignoring a reminder + Given I provide credentials that are due for a reminder + And I have logged in + When I click the remind-me-later button + Then I should end up at my intended destination + + Examples: + | category | nag type | + | mfa | add | + | method | add | + | profile | review | + + Scenario: Ensuring that manager mfa data is not displayed to the user + Given I provide credentials for a user that has used the manager mfa option + And I have logged in + Then I should see the message: "Please take a moment to review" + And I should not see any manager mfa information diff --git a/local.env.dist b/local.env.dist index c847edfd..ac3f00f7 100644 --- a/local.env.dist +++ b/local.env.dist @@ -59,3 +59,18 @@ SHOW_SAML_ERRORS= THEME_USE= TIMEZONE= XDEBUG_REMOTE_HOST= + +# expirychecker config + +# The URL to send a user to for changing their password. +# Example: https://pw.example.com/#/password/create +PASSWORD_CHANGE_URL= + +# profilereview config + +# The URL to send a user to for setting up their profile. +# Example: https://pw.example.com/#/ +PROFILE_URL= + +# An absolute URL the user can go to to learn more about MFA. +MFA_LEARN_MORE_URL= diff --git a/modules/profilereview/lib/Assert.php b/modules/profilereview/lib/Assert.php new file mode 100644 index 00000000..f20dff99 --- /dev/null +++ b/modules/profilereview/lib/Assert.php @@ -0,0 +1,55 @@ +initComposerAutoloader(); + assert('is_array($config)'); + + $this->loggerClass = $config['loggerClass'] ?? Psr3SamlLogger::class; + $this->logger = LoggerFactory::get($this->loggerClass); + + $this->loadValuesFromConfig($config, [ + 'profileUrl', + 'employeeIdAttr', + ]); + + $this->mfaLearnMoreUrl = $config['mfaLearnMoreUrl'] ?? null; + $this->profileUrl = $config['profileUrl'] ?? null; + } + + /** + * @param $config + * @param $attributes + * @throws \Exception + */ + protected function loadValuesFromConfig($config, $attributes) + { + foreach ($attributes as $attribute) { + $this->$attribute = $config[$attribute] ?? null; + + self::validateConfigValue( + $attribute, + $this->$attribute, + $this->logger + ); + } + } + + /** + * Validate the given config value + * + * @param string $attribute The name of the attribute. + * @param mixed $value The value to check. + * @param LoggerInterface $logger The logger. + * @throws \Exception + */ + public static function validateConfigValue($attribute, $value, $logger) + { + if (empty($value) || !is_string($value)) { + $exception = new \Exception(sprintf( + 'The value we have for %s (%s) is empty or is not a string', + $attribute, + var_export($value, true) + ), 1507146042); + + $logger->critical($exception->getMessage()); + throw $exception; + } + } + + /** + * Get the specified attribute from the given state data. + * + * NOTE: If the attribute's data is an array, the first value will be + * returned. Otherwise, the attribute's data will simply be returned + * as-is. + * + * @param string $attributeName The name of the attribute. + * @param array $state The state data. + * @return mixed The attribute value, or null if not found. + */ + protected function getAttribute($attributeName, $state) + { + $attributeData = $state['Attributes'][$attributeName] ?? null; + + if (is_array($attributeData)) { + return $attributeData[0] ?? null; + } + + return $attributeData; + } + + /** + * Get all of the values for the specified attribute from the given state + * data. + * + * NOTE: If the attribute's data is an array, it will be returned as-is. + * Otherwise, it will be returned as a single-entry array of the data. + * + * @param string $attributeName The name of the attribute. + * @param array $state The state data. + * @return array|null The attribute's value(s), or null if the attribute was + * not found. + */ + protected function getAttributeAllValues($attributeName, $state) + { + $attributeData = $state['Attributes'][$attributeName] ?? null; + + return is_null($attributeData) ? null : (array)$attributeData; + } + + /** + * Return the saml:RelayState if it begins with "http" or "https". Otherwise + * return an empty string. + * + * @param array $state + * @returns string + * @return mixed|string + */ + protected static function getRelayStateUrl($state) + { + if (array_key_exists('saml:RelayState', $state)) { + $samlRelayState = $state['saml:RelayState']; + + if (strpos($samlRelayState, "http://") === 0) { + return $samlRelayState; + } + + if (strpos($samlRelayState, "https://") === 0) { + return $samlRelayState; + } + } + return ''; + } + + protected function initComposerAutoloader() + { + $path = __DIR__ . '/../../../vendor/autoload.php'; + if (file_exists($path)) { + require_once $path; + } + } + + protected static function isHeadedToProfileUrl($state, $ProfileUrl) + { + if (array_key_exists('saml:RelayState', $state)) { + $currentDestination = self::getRelayStateUrl($state); + if (! empty($currentDestination)) { + return (strpos($currentDestination, $ProfileUrl) === 0); + } + } + return false; + } + + /** + * Redirect the user to set up profile. + * + * @param array $state + */ + public static function redirectToProfile(&$state) + { + $profileUrl = $state['ProfileUrl']; + // Tell the profile-setup URL where the user is ultimately trying to go (if known). + $currentDestination = self::getRelayStateUrl($state); + if (! empty($currentDestination)) { + $profileUrl = HTTP::addURLParameters( + $profileUrl, + ['returnTo' => $currentDestination] + ); + } + + $logger = LoggerFactory::getAccordingToState($state); + $logger->warning(json_encode([ + 'module' => 'profilereview', + 'event' => 'redirect to profile', + 'employeeId' => $state['employeeId'], + 'profileUrl' => $profileUrl, + ])); + + HTTP::redirectTrustedURL($profileUrl); + } + + /** + * Apply this AuthProc Filter. It will either return (indicating that it + * has completed) or it will redirect the user, in which case it will + * later call `SimpleSAML\Auth\ProcessingChain::resumeProcessing($state)`. + * + * @param array &$state The current state. + */ + public function process(&$state) + { + // Get the necessary info from the state data. + $employeeId = $this->getAttribute($this->employeeIdAttr, $state); + $isHeadedToProfileUrl = self::isHeadedToProfileUrl($state, $this->profileUrl); + + $mfa = $this->getAttributeAllValues('mfa', $state); + $method = $this->getAttributeAllValues('method', $state); + $profileReview = $this->getAttribute('profile_review', $state); + + if (! $isHeadedToProfileUrl) { + // Record to the state what logger class to use. + $state['loggerClass'] = $this->loggerClass; + + $state['ProfileUrl'] = $this->profileUrl; + + if (self::needToShow($mfa['add'], self::MFA_ADD_PAGE)) { + $this->redirectToNag($state, $employeeId, self::MFA_ADD_PAGE); + } + + if (self::needToShow($method['add'], self::METHOD_ADD_PAGE)) { + $this->redirectToNag($state, $employeeId, self::METHOD_ADD_PAGE); + } + + if (self::needToShow($profileReview, self::REVIEW_PAGE)) + { + $this->redirectToProfileReview($state, $employeeId, $mfa['options'], $method['options']); + } + } + + $this->logger->warning(json_encode([ + 'module' => 'profilereview', + 'event' => 'no nag/review needed', + 'isHeadedToProfileUrl' => $isHeadedToProfileUrl, + 'profileReview' => $profileReview, + 'mfa.add' => $mfa['add'], + 'method.add' => $method['add'], + 'employeeId' => $employeeId, + ])); + + unset($state['Attributes']['method']); + unset($state['Attributes']['mfa']); + return; + } + + /** + * Redirect user to profile review page unless there is nothing to review + * + * @param array $state The state data. + * @param string $employeeId The Employee ID of the user account. + * @param array $mfaOptions A list of the mfa options. + * @param array $methodOptions A list of the method options. + */ + protected function redirectToProfileReview(&$state, $employeeId, $mfaOptions, $methodOptions) + { + assert('is_array($state)'); + + foreach ($mfaOptions as $key => $mfaOption) { + if ($mfaOption['type'] === 'manager') { + unset ($mfaOptions[$key]); + } + } + + if (count($mfaOptions) == 0 && count($methodOptions) == 0) { + return; + } + + /* Save state and redirect. */ + $state['employeeId'] = $employeeId; + $state['profileUrl'] = $this->profileUrl; + $state['mfaOptions'] = $mfaOptions; + $state['methodOptions'] = $methodOptions; + $state['template'] = 'review.php'; + + $stateId = State::saveState($state, self::STAGE_SENT_TO_NAG); + $url = Module::getModuleURL('profilereview/nag.php'); + + HTTP::redirectTrustedURL($url, array('StateId' => $stateId)); + } + + /** + * @param array $state + * @param string $employeeId + * @param string $template + */ + protected function redirectToNag(&$state, $employeeId, $template) + { + /* Save state and redirect. */ + $state['employeeId'] = $employeeId; + $state['mfaLearnMoreUrl'] = $this->mfaLearnMoreUrl; + $state['profileUrl'] = $this->profileUrl; + $state['template'] = $template; + + $stateId = State::saveState($state, self::STAGE_SENT_TO_NAG); + $url = Module::getModuleURL('profilereview/nag.php'); + + HTTP::redirectTrustedURL($url, array('StateId' => $stateId)); + } + + public static function hasSeenSplashPageRecently(string $page) + { + $session = Session::getSessionFromRequest(); + return (bool)$session->getData( + self::SESSION_TYPE, + $page + ); + } + + public static function skipSplashPagesFor($seconds, string $page) + { + $session = Session::getSessionFromRequest(); + $session->setData( + self::SESSION_TYPE, + $page, + true, + $seconds + ); + $session->save(); + } + + public static function needToShow($flag, $page) + { + $oneDay = 24 * 60 * 60; + if ($flag === 'yes' && ! self::hasSeenSplashPageRecently($page)) { + self::skipSplashPagesFor($oneDay, $page); + return true; + } + return false; + } +} diff --git a/modules/profilereview/lib/LoggerFactory.php b/modules/profilereview/lib/LoggerFactory.php new file mode 100644 index 00000000..83e335d1 --- /dev/null +++ b/modules/profilereview/lib/LoggerFactory.php @@ -0,0 +1,39 @@ +data['header'] = 'Set up Recovery Methods'; +$this->includeAtTemplateBase('includes/header.php'); +?> +

+ Did you know you can provide alternate email addresses for password recovery? +

+

+ We highly encourage you to do this to ensure continuous access and improved security. +

+
+ + + +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/profilereview/templates/nag-for-mfa.php b/modules/profilereview/templates/nag-for-mfa.php new file mode 100644 index 00000000..1e31541e --- /dev/null +++ b/modules/profilereview/templates/nag-for-mfa.php @@ -0,0 +1,28 @@ +data['header'] = 'Set up 2-Step Verification'; +$this->includeAtTemplateBase('includes/header.php'); + +$mfaLearnMoreUrl = $this->data['mfaLearnMoreUrl']; +?> +

+ Did you know you could greatly increase the security of your account by enabling 2-Step Verification? +

+

+ We highly encourage you to do this for your own safety. +

+
+ + + + + +

Learn more

+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/profilereview/templates/review.php b/modules/profilereview/templates/review.php new file mode 100644 index 00000000..365d908d --- /dev/null +++ b/modules/profilereview/templates/review.php @@ -0,0 +1,62 @@ +data['header'] = 'Review 2-Step Verification and Password Recovery'; +$this->includeAtTemplateBase('includes/header.php'); + +$profileUrl = $this->data['profileUrl']; + +?> +

+ Please take a moment to review your 2-Step Verification options and + Password Recovery Methods. +

+

+ We highly encourage you to do this for your own safety. +

+

2-Step Verification

+ + + + + + + + data['mfaOptions'] as $option): ?> + + + + + + + +
LabelTypeCreatedLast Used
+

Password Recovery Methods

+ + + + + + + data['methodOptions'] as $option): ?> + + + + + + +
EmailVerifiedCreated
+
+ + + + + +

Go to Profile

+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/profilereview/www/nag.php b/modules/profilereview/www/nag.php new file mode 100644 index 00000000..7c0e91c0 --- /dev/null +++ b/modules/profilereview/www/nag.php @@ -0,0 +1,45 @@ +data['profileUrl'] = $state['profileUrl']; +$t->data['methodOptions'] = $state['methodOptions']; +$t->data['mfaOptions'] = $state['mfaOptions']; +$t->data['mfaLearnMoreUrl'] = $state['mfaLearnMoreUrl']; +$t->show(); + +$logger->warning(json_encode([ + 'module' => 'profilereview', + 'event' => 'presented nag', + 'template' => $state['template'], + 'employeeId' => $state['employeeId'], +])); diff --git a/modules/profilereview/www/review.php b/modules/profilereview/www/review.php new file mode 100644 index 00000000..aa0cae04 --- /dev/null +++ b/modules/profilereview/www/review.php @@ -0,0 +1,47 @@ +data['profileUrl'] = $state['profileUrl']; +$t->data['methodOptions'] = $state['methodOptions']; +$t->data['mfaOptions'] = $state['mfaOptions']; +$t->show(); + +$logger->warning(json_encode([ + 'module' => 'profilereview', + 'event' => 'presented profile review', + 'employeeId' => $state['employeeId'], +]));