From b3d3f3d3f59d7c6063838324541d8b9afbc69307 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:36:22 -0600 Subject: [PATCH 01/92] copy content of the expirychecker module https://github.com/silinternational/simplesamlphp-module-expirychecker --- README.md | 74 +++ features/bootstrap/ExpiryContext.php | 257 +++++++++++ .../lib/Auth/Process/ExpiryDate.php | 420 ++++++++++++++++++ modules/expirychecker/lib/Utilities.php | 92 ++++ modules/expirychecker/src/Validator.php | 71 +++ .../expirychecker/templates/about2expire.php | 48 ++ modules/expirychecker/templates/expired.php | 38 ++ modules/expirychecker/www/about2expire.php | 65 +++ modules/expirychecker/www/expired.php | 56 +++ 9 files changed, 1121 insertions(+) create mode 100644 features/bootstrap/ExpiryContext.php create mode 100644 modules/expirychecker/lib/Auth/Process/ExpiryDate.php create mode 100644 modules/expirychecker/lib/Utilities.php create mode 100644 modules/expirychecker/src/Validator.php create mode 100644 modules/expirychecker/templates/about2expire.php create mode 100644 modules/expirychecker/templates/expired.php create mode 100644 modules/expirychecker/www/about2expire.php create mode 100644 modules/expirychecker/www/expired.php diff --git a/README.md b/README.md index 19c2e4dc..4986cd25 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,77 @@ RUN cd /data/vendor/simplesamlphp/simplesamlphp/modules/material/dictionaries/ov ## Misc. Notes * Use of sildisco's LogUser module is optional and triggered via an authproc. + +## Included Modules + +### ExpiryChecker simpleSAMLphp Module +A simpleSAMLphp module for warning users that their password will expire soon +or that it has already expired. + +**NOTE:** This module does *not* prevent the user from logging in. It merely +shows a warning page (if their password is about to expire), with the option to +change their password now or later, or it tells the user that their password has +already expired, with the only option being to go change their password now. +Both of these pages will be bypassed (for varying lengths of time) if the user +has recently seen one of those two pages, in order to allow the user to get to +the change-password website (assuming it is also behind this IdP). If the user +should not be allowed to log in at all, the simpleSAMLphp Auth. Source should +consider the credentials provided by the user to be invalid. + +The expirychecker module is implemented as an Authentication Processing Filter, +or AuthProc. That means it can be configured in the global config.php file or +the SP remote or IdP hosted metadata. + +It is recommended to run the expirychecker module at the IdP, and configure the +filter to run before all the other filters you may have enabled. + +#### How to use the module + +Set filter parameters in your config. We recommend adding +them to the `'authproc'` array in your `metadata/saml20-idp-hosted.php` file, +but you are also able to put them in the `'authproc.idp'` array in your +`config/config.php` file. + +Example (in `metadata/saml20-idp-hosted.php`): + + 'authproc' => [ + 10 => [ + // Required: + 'class' => 'expirychecker:ExpiryDate', + 'accountNameAttr' => 'cn', + 'expiryDateAttr' => 'schacExpiryDate', + 'passwordChangeUrl' => 'https://idm.example.com/pwdmgr/', + + // Optional: + 'warnDaysBefore' => 14, + 'originalUrlParam' => 'originalurl', + 'dateFormat' => 'm.d.Y', // Use PHP's date syntax. + 'loggerClass' => '\\Sil\\Psr3Adapters\\Psr3SamlLogger', + ], + + // ... + ], + +The `accountNameAttr` parameter represents the SAML attribute name which has +the user's account name stored in it. In certain situations, this will be +displayed to the user, as well as being used in log messages. + +The `expiryDateAttr` parameter represents the SAML attribute name which has +the user's expiry date, which must be formated as YYYYMMDDHHMMSSZ (e.g. +`20111011235959Z`). Those two attributes need to be part of the attribute set +returned when the user successfully authenticates. + +The `warnDaysBefore` parameter should be an integer representing how many days +before the expiry date the "about to expire" warning will be shown to the user. + +The `dateFormat` parameter specifies how you want the date to be formatted, +using PHP `date()` syntax. See . + +The `loggerClass` parameter specifies the name of a PSR-3 compatible class that +can be autoloaded, to use as the logger within ExpiryDate. + +#### Acknowledgements + +This is adapted from the `ssp-iidp-expirycheck` and `expirycheck` modules. +Thanks to Alex Mihičinac, Steve Moitozo, and Steve Bagwell for the initial work +they did on those two modules. diff --git a/features/bootstrap/ExpiryContext.php b/features/bootstrap/ExpiryContext.php new file mode 100644 index 00000000..5017ba12 --- /dev/null +++ b/features/bootstrap/ExpiryContext.php @@ -0,0 +1,257 @@ +session = new Session($driver); + $this->session->start(); + } + + /** + * Assert that the given page has a form that contains the given text. + * + * @param string $text The text (or HTML) to search for. + * @param DocumentElement $page The page to search in. + * @return void + */ + protected function assertFormContains($text, $page) + { + $forms = $page->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() + )); + } + + /** + * Assert that the given page does NOT have a form that contains the given + * text. + * + * @param string $text The text (or HTML) to search for. + * @param DocumentElement $page The page to search in. + * @return void + */ + protected function assertFormNotContains($text, $page) + { + $forms = $page->findAll('css', 'form'); + foreach ($forms as $form) { + if (strpos($form->getHtml(), $text) !== false) { + Assert::fail(sprintf( + "Found a form 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; + } + + /** + * @Given I provide credentials that will expire in the distant future + */ + public function iProvideCredentialsThatWillExpireInTheDistantFuture() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'distant_future'; + $this->password = 'a'; + } + + /** + * @When I login + */ + public function iLogin() + { + $this->session->visit('http://sp/module.php/core/authenticate.php?as=ssp-hub-idp'); + $page = $this->session->getPage(); + $page->fillField('username', $this->username); + $page->fillField('password', $this->password); + $this->submitLoginForm($page); + } + + /** + * @Then I should end up at my intended destination + */ + public function iShouldEndUpAtMyIntendedDestination() + { + $page = $this->session->getPage(); + Assert::assertContains('Your attributes', $page->getHtml()); + } + + /** + * @Given I provide credentials that will expire very soon + */ + public function iProvideCredentialsThatWillExpireVerySoon() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'near_future'; + $this->password = 'b'; + } + + /** + * @Then I should see a warning that my password will expire soon + */ + public function iShouldSeeAWarningThatMyPasswordWillExpireSoon() + { + $page = $this->session->getPage(); + Assert::assertContains('will expire', $page->getHtml()); + } + + /** + * Submit the login form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitLoginForm($page) + { + $loginButton = $this->getLoginButton($page); + $loginButton->click(); + + // SimpleSAMLphp 1.15 markup for secondary page: + $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); + if ($postLoginSubmitButton instanceof NodeElement) { + $postLoginSubmitButton->click(); + } else { + + // SimpleSAMLphp 1.14 markup for secondary page: + $body = $page->find('css', 'body'); + if ($body instanceof NodeElement) { + $onload = $body->getAttribute('onload'); + if ($onload === "document.getElementsByTagName('input')[0].click();") { + $body->pressButton('Submit'); + } + } + } + } + + /** + * @Then there should be a way to go change my password now + */ + public function thereShouldBeAWayToGoChangeMyPasswordNow() + { + $page = $this->session->getPage(); + $this->assertFormContains('change', $page); + } + + /** + * @Then there should be a way to continue without changing my password + */ + public function thereShouldBeAWayToContinueWithoutChangingMyPassword() + { + $page = $this->session->getPage(); + $this->assertFormContains('continue', $page); + } + + /** + * @Given I provide credentials that have expired + */ + public function iProvideCredentialsThatHaveExpired() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'already_past'; + $this->password = 'c'; + } + + /** + * @Then I should see a message that my password has expired + */ + public function iShouldSeeAMessageThatMyPasswordHasExpired() + { + $page = $this->session->getPage(); + Assert::assertContains('has expired', $page->getHtml()); + } + + /** + * @Then there should NOT be a way to continue without changing my password + */ + public function thereShouldNotBeAWayToContinueWithoutChangingMyPassword() + { + $page = $this->session->getPage(); + $this->assertFormNotContains('continue', $page); + } + + /** + * @Given I provide credentials that have no password expiration date + */ + public function iProvideCredentialsThatHaveNoPasswordExpirationDate() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'missing_exp'; + $this->password = 'd'; + } + + /** + * @Then I should see an error message + */ + public function iShouldSeeAnErrorMessage() + { + $page = $this->session->getPage(); + Assert::assertContains('An error occurred', $page->getHtml()); + } + + /** + * @Given I provide credentials that have an invalid password expiration date + */ + public function iProvideCredentialsThatHaveAnInvalidPasswordExpirationDate() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'invalid_exp'; + $this->password = 'e'; + } +} diff --git a/modules/expirychecker/lib/Auth/Process/ExpiryDate.php b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php new file mode 100644 index 00000000..fbfd7fd4 --- /dev/null +++ b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php @@ -0,0 +1,420 @@ +initComposerAutoloader(); + + assert('is_array($config)'); + + $this->initLogger($config); + + $this->loadValuesFromConfig($config, [ + 'warnDaysBefore' => [ + Validator::INT, + ], + 'originalUrlParam' => [ + Validator::STRING, + Validator::NOT_EMPTY, + ], + 'passwordChangeUrl' => [ + Validator::STRING, + Validator::NOT_EMPTY, + ], + 'accountNameAttr' => [ + Validator::STRING, + Validator::NOT_EMPTY, + ], + 'expiryDateAttr' => [ + Validator::STRING, + Validator::NOT_EMPTY, + ], + 'dateFormat' => [ + Validator::STRING, + Validator::NOT_EMPTY, + ], + ]); + } + + protected function loadValuesFromConfig($config, $attributeRules) + { + foreach ($attributeRules as $attribute => $rules) { + if (array_key_exists($attribute, $config)) { + $this->$attribute = $config[$attribute]; + } + + Validator::validate($this->$attribute, $rules, $this->logger, $attribute); + } + } + + /** + * 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; + } + + /** + * Calculate how many days remain between now and when the password will + * expire. + * + * @param int $expiryTimestamp The timestamp for when the password will + * expire. + * @return int The number of days remaining + */ + protected function getDaysLeftBeforeExpiry($expiryTimestamp) + { + $now = time(); + $end = $expiryTimestamp; + return round(($end - $now) / (24*60*60)); + } + + /** + * Get the timestamp for when the user's password will expire, throwing an + * exception if unable to do so. + * + * @param string $expiryDateAttr The name of the attribute where the + * expiration date (as a string) is stored. + * @param array $state The state data. + * @return int The expiration timestamp. + * @throws \Exception + */ + protected function getExpiryTimestamp($expiryDateAttr, $state) + { + $expiryDateString = $this->getAttribute($expiryDateAttr, $state); + + // Ensure that EVERY user login provides a usable password expiration date. + $expiryTimestamp = strtotime($expiryDateString) ?: null; + if (empty($expiryTimestamp)) { + throw new \Exception(sprintf( + "We could not understand the expiration date (%s, from %s) for " + . "the user's password, so we do not know whether their " + . "password is still valid.", + var_export($expiryDateString, true), + var_export($expiryDateAttr, true) + ), 1496843359); + } + return $expiryTimestamp; + } + + public static function hasSeenSplashPageRecently() + { + $session = Session::getSessionFromRequest(); + return (bool)$session->getData( + self::SESSION_TYPE, + self::HAS_SEEN_SPLASH_PAGE + ); + } + + public static function skipSplashPagesFor($seconds) + { + $session = Session::getSessionFromRequest(); + $session->setData( + self::SESSION_TYPE, + self::HAS_SEEN_SPLASH_PAGE, + true, + $seconds + ); + $session->save(); + } + + protected function initComposerAutoloader() + { + $path = __DIR__ . '/../../../vendor/autoload.php'; + if (file_exists($path)) { + require_once $path; + } + } + + protected function initLogger($config) + { + $loggerClass = $config['loggerClass'] ?? Psr3SamlLogger::class; + $this->logger = new $loggerClass(); + if (! $this->logger instanceof LoggerInterface) { + throw new \Exception(sprintf( + 'The specified loggerClass (%s) does not implement ' + . '\\Psr\\Log\\LoggerInterface.', + var_export($loggerClass, true) + ), 1496928725); + } + } + + /** + * See if the given timestamp is in the past. + * + * @param int $timestamp The timestamp to check. + * @return bool + */ + public function isDateInPast(int $timestamp) + { + return ($timestamp < time()); + } + + /** + * Check whether the user's password has expired. + * + * @param int $expiryTimestamp The timestamp for when the user's password + * will expire. + * @return bool + */ + public function isExpired(int $expiryTimestamp) + { + return $this->isDateInPast($expiryTimestamp); + } + + /** + * Check whether it's time to warn the user that they will need to change + * their password soon. + * + * @param int $expiryTimestamp The timestamp for when the password expires. + * @param int $warnDaysBefore How many days before the expiration we should + * warn the user. + * @return boolean + */ + public function isTimeToWarn($expiryTimestamp, $warnDaysBefore) + { + $daysLeft = $this->getDaysLeftBeforeExpiry($expiryTimestamp); + return ($daysLeft <= $warnDaysBefore); + } + + /** + * Redirect the user to the change password url if they haven't gone + * there in the last 10 minutes + * @param array $state + * @param string $accountName + * @param string $passwordChangeUrl + * @param string $change_pwd_session + * @param int $expiryTimestamp The timestamp when the password will expire. + */ + public function redirect2PasswordChange( + &$state, + $accountName, + $passwordChangeUrl, + $change_pwd_session, + $expiryTimestamp + ) { + $sessionType = 'expirychecker'; + /* Save state and redirect. */ + $state['expiresAtTimestamp'] = $expiryTimestamp; + $state['accountName'] = $accountName; + $id = State::saveState( + $state, + 'expirychecker:redirected_to_password_change_url' + ); + $ignoreMinutes = 60; + + $session = Session::getSessionFromRequest(); + $idpExpirySession = $session->getData($sessionType, $change_pwd_session); + + // If the session shows that the User already passed this way, + // don't redirect to change password page + if ($idpExpirySession !== null) { + ProcessingChain::resumeProcessing($state); + } else { + // Otherwise, set a value to tell us they've probably changed + // their password, in order to allow password to get propagated + $session->setData( + $sessionType, + $change_pwd_session, + 1, + (60 * $ignoreMinutes) + ); + $session->save(); + } + + + /* If state already has the change password url, go straight there to + * avoid eternal loop between that and the idp. Otherwise add the + * original destination url as a parameter. */ + if (array_key_exists('saml:RelayState', $state)) { + $relayState = $state['saml:RelayState']; + if (strpos($relayState, $passwordChangeUrl) !== false) { + ProcessingChain::resumeProcessing($state); + } else { + $returnTo = Utilities::getUrlFromRelayState( + $relayState + ); + if (! empty($returnTo)) { + $passwordChangeUrl .= '?returnTo=' . $returnTo; + } + } + } + + $this->logger->warning(json_encode([ + 'event' => 'expirychecker: redirecting to change password', + 'accountName' => $accountName, + 'passwordChangeUrl' => $passwordChangeUrl, + ])); + + HTTP::redirectTrustedURL($passwordChangeUrl, array()); + } + + /** + * Apply this AuthProc Filter. + * + * @param array &$state The current state. + */ + public function process(&$state) + { + $employeeId = $this->getAttribute($this->employeeIdAttr, $state); + + /* If the user has already seen a splash page from this AuthProc + * recently, simply let them pass on through (so they can get into the + * change-password website, for example). */ + if (self::hasSeenSplashPageRecently()) { + $this->logger->warning(json_encode([ + 'event' => 'expirychecker: skip message, seen recently', + 'employeeId' => $employeeId, + ])); + return; + } + + // Get the necessary info from the state data. + $accountName = $this->getAttribute($this->accountNameAttr, $state); + $expiryTimestamp = $this->getExpiryTimestamp($this->expiryDateAttr, $state); + + $this->logger->warning(json_encode([ + 'event' => 'expirychecker: will check expiration date', + 'employeeId' => $employeeId, + 'accountName' => $accountName, + 'expiryDateAttrValue' => $this->getAttribute($this->expiryDateAttr, $state), + 'expiryTimestamp' => $expiryTimestamp, + ])); + + if ($this->isExpired($expiryTimestamp)) { + $this->redirectToExpiredPage($state, $accountName, $expiryTimestamp); + } + + // Display a password expiration warning page if it's time to do so. + if ($this->isTimeToWarn($expiryTimestamp, $this->warnDaysBefore)) { + $this->redirectToWarningPage($state, $accountName, $expiryTimestamp); + } + + $this->logger->warning(json_encode([ + 'event' => 'expirychecker: no action necessary', + 'employeeId' => $employeeId, + ])); + } + + /** + * Redirect the user to the expired-password page. + * + * @param array $state The state data. + * @param string $accountName The name of the user account. + * @param int $expiryTimestamp When the password expired. + */ + public function redirectToExpiredPage(&$state, $accountName, $expiryTimestamp) + { + assert('is_array($state)'); + + $this->logger->warning(json_encode([ + 'event' => 'expirychecker: password expired', + 'accountName' => $accountName, + ])); + + /* Save state and redirect. */ + $state['expiresAtTimestamp'] = $expiryTimestamp; + $state['accountName'] = $accountName; + $state['passwordChangeUrl'] = $this->passwordChangeUrl; + $state['originalUrlParam'] = $this->originalUrlParam; + + $id = State::saveState($state, 'expirychecker:expired'); + $url = Module::getModuleURL('expirychecker/expired.php'); + + HTTP::redirectTrustedURL($url, array('StateId' => $id)); + } + + /** + * Redirect the user to the warning page. + * + * @param array $state The state data. + * @param string $accountName The name of the user account. + * @param int $expiryTimestamp When the password will expire. + */ + protected function redirectToWarningPage(&$state, $accountName, $expiryTimestamp) + { + assert('is_array($state)'); + + $this->logger->warning(json_encode([ + 'event' => 'expirychecker: about to expire', + 'accountName' => $accountName, + ])); + + $daysLeft = $this->getDaysLeftBeforeExpiry($expiryTimestamp); + $state['daysLeft'] = $daysLeft; + + if (isset($state['isPassive']) && $state['isPassive'] === true) { + /* We have a passive request. Skip the warning. */ + return; + } + + /* Save state and redirect. */ + $state['expiresAtTimestamp'] = $expiryTimestamp; + $state['accountName'] = $accountName; + $state['passwordChangeUrl'] = $this->passwordChangeUrl; + $state['originalUrlParam'] = $this->originalUrlParam; + + $id = State::saveState($state, 'expirychecker:about2expire'); + $url = Module::getModuleURL('expirychecker/about2expire.php'); + + HTTP::redirectTrustedURL($url, array('StateId' => $id)); + } +} diff --git a/modules/expirychecker/lib/Utilities.php b/modules/expirychecker/lib/Utilities.php new file mode 100644 index 00000000..0e2b80b6 --- /dev/null +++ b/modules/expirychecker/lib/Utilities.php @@ -0,0 +1,92 @@ +critical($exception->getMessage()); + throw $exception; + } + } + } + + /** + * See if the given value satisfies the specified rule. + * + * @param mixed $value The value to check. + * @param string $rule The rule (see this class's constants). + * @param LoggerInterface $logger The logger. + * @return bool + * @throws InvalidArgumentException + */ + protected static function isValid($value, $rule, $logger) + { + switch ($rule) { + case self::INT: + return is_int($value); + + case self::NOT_EMPTY: + return !empty($value); + + case self::STRING: + return is_string($value); + + default: + $exception = new InvalidArgumentException(sprintf( + 'Unknown validation rule: %s', + var_export($rule, true) + ), 1496866914); + + $logger->critical($exception->getMessage()); + throw $exception; + } + } +} diff --git a/modules/expirychecker/templates/about2expire.php b/modules/expirychecker/templates/about2expire.php new file mode 100644 index 00000000..468faeaf --- /dev/null +++ b/modules/expirychecker/templates/about2expire.php @@ -0,0 +1,48 @@ +data['header'] = sprintf( + 'Your password will expire in %s %s', + $this->data['daysLeft'], + $this->data['dayOrDays'] +); +$this->data['autofocus'] = 'yesbutton'; + +$this->includeAtTemplateBase('includes/header.php'); + +$dateString = msgfmt_format_message( + $this->getLanguage(), + '{0,date,long}', + [$this->data['expiresAtTimestamp']] +); + +?> +

+ The password for your data['accountName']); ?> + account will expire on . +

+

+ Would you like to update your password now? +

+ +
+ + data['formData'] as $name => $value): ?> + + + + + + +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/expirychecker/templates/expired.php b/modules/expirychecker/templates/expired.php new file mode 100644 index 00000000..8b46d306 --- /dev/null +++ b/modules/expirychecker/templates/expired.php @@ -0,0 +1,38 @@ +data['header'] = 'Your password has expired'; + +$this->includeAtTemplateBase('includes/header.php'); + +$dateString = msgfmt_format_message( + $this->getLanguage(), + '{0,date,long}', + [$this->data['expiresAtTimestamp']] +); + +?> +

+ The password for your data['accountName']); ?> + account expired on . +

+

+ You will need to update your password before you can continue to where you + were going. +

+

+

+ + data['formData'] as $name => $value): ?> + + + + +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/expirychecker/www/about2expire.php b/modules/expirychecker/www/about2expire.php new file mode 100644 index 00000000..9e38ac03 --- /dev/null +++ b/modules/expirychecker/www/about2expire.php @@ -0,0 +1,65 @@ +data['formTarget'] = Module::getModuleURL('expirychecker/about2expire.php'); +$t->data['formData'] = ['StateId' => $stateId]; +$t->data['daysLeft'] = $state['daysLeft']; +$t->data['dayOrDays'] = (intval($state['daysLeft']) === 1 ? 'day' : 'days'); +$t->data['expiresAtTimestamp'] = $state['expiresAtTimestamp']; +$t->data['accountName'] = $state['accountName']; +$t->show(); + +Logger::info('expirychecker - User has been warned that their password will expire soon.'); diff --git a/modules/expirychecker/www/expired.php b/modules/expirychecker/www/expired.php new file mode 100644 index 00000000..c4c0b8c9 --- /dev/null +++ b/modules/expirychecker/www/expired.php @@ -0,0 +1,56 @@ +data['formTarget'] = Module::getModuleURL('expirychecker/expired.php'); +$t->data['formData'] = ['StateId' => $stateId]; +$t->data['expiresAtTimestamp'] = $state['expiresAtTimestamp']; +$t->data['accountName'] = $state['accountName']; +$t->show(); + +Logger::info('expirychecker - User has been told that their password has expired.'); From d1bfa729271697a8e38fa46904bff6c785e1d8fb Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:56:00 -0600 Subject: [PATCH 02/92] use the local expirychecker code and remove from composer --- Dockerfile | 6 ++++- composer.json | 1 - composer.lock | 55 ++-------------------------------------------- docker-compose.yml | 22 ++++++++++++++++++- 4 files changed, 28 insertions(+), 56 deletions(-) diff --git a/Dockerfile b/Dockerfile index af1012b3..1c701fdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,8 +39,12 @@ COPY composer.lock /data/ RUN composer self-update --no-interaction RUN COMPOSER_ALLOW_SUPERUSER=1 composer install --prefer-dist --no-interaction --no-dev --optimize-autoloader --no-scripts --no-progress -# Copy in SSP override files ENV SSP_PATH /data/vendor/simplesamlphp/simplesamlphp + +# Copy modules into simplesamlphp +COPY modules/ $SSP_PATH/modules + +# Copy in SSP override files RUN mv $SSP_PATH/www/index.php $SSP_PATH/www/ssp-index.php COPY dockerbuild/ssp-overrides/index.php $SSP_PATH/www/index.php RUN mv $SSP_PATH/www/saml2/idp/SingleLogoutService.php $SSP_PATH/www/saml2/idp/ssp-SingleLogoutService.php diff --git a/composer.json b/composer.json index b50f58c2..da75894e 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "ext-memcached": "*", "simplesamlphp/simplesamlphp": "^1.19.6", "simplesamlphp/composer-module-installer": "1.1.8", - "silinternational/simplesamlphp-module-expirychecker": "^3.1.0", "silinternational/simplesamlphp-module-silauth": "^7.1.1", "silinternational/simplesamlphp-module-mfa": "^5.2.1", "silinternational/simplesamlphp-module-profilereview": "^2.1.0", diff --git a/composer.lock b/composer.lock index c987192b..1eb2f63e 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": "df01bb039aaf6b25519b9e6dea6dae58", + "content-hash": "aa7c6c86f5fefed69d818d69b1a506c3", "packages": [ { "name": "aws/aws-crt-php", @@ -2942,57 +2942,6 @@ }, "time": "2022-08-24T14:44:38+00:00" }, - { - "name": "silinternational/simplesamlphp-module-expirychecker", - "version": "3.1.3", - "source": { - "type": "git", - "url": "https://github.com/silinternational/simplesamlphp-module-expirychecker.git", - "reference": "2a8f8b18fe60ba3e0e3d7e9c039bcf7347bddb29" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silinternational/simplesamlphp-module-expirychecker/zipball/2a8f8b18fe60ba3e0e3d7e9c039bcf7347bddb29", - "reference": "2a8f8b18fe60ba3e0e3d7e9c039bcf7347bddb29", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.2", - "silinternational/psr3-adapters": "^1.1 || ^2.0 || ^3.0", - "simplesamlphp/simplesamlphp": "~1.17.7 || ~1.18.4 || ~1.19.0" - }, - "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\\SspExpiryChecker\\": "src/", - "Sil\\SspExpiryChecker\\Behat\\": "features/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1" - ], - "authors": [ - { - "name": "Matt Henderson", - "email": "matt_henderson@sil.org" - } - ], - "description": "simpleSAMLphp module for warning users that their password will expire soon or has already expired.", - "support": { - "issues": "https://github.com/silinternational/simplesamlphp-module-expirychecker/issues", - "source": "https://github.com/silinternational/simplesamlphp-module-expirychecker/tree/3.1.3" - }, - "time": "2023-02-08T15:53:15+00:00" - }, { "name": "silinternational/simplesamlphp-module-material", "version": "8.1.1", @@ -10036,5 +9985,5 @@ "ext-memcached": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.yml b/docker-compose.yml index a374ffdf..2f0b2267 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: ssp: build: . volumes: - # Utilize custom certs + # Utilize custom certs - ./development/ssp/cert:/data/vendor/simplesamlphp/simplesamlphp/cert # Utilize custom configs @@ -11,6 +11,10 @@ services: # Configure the debugger - ./development/ssp/run-debug.sh:/data/run-debug.sh + + # Local modules + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + command: ["/data/run-debug.sh"] ports: - "80:80" @@ -46,6 +50,7 @@ services: - ./dockerbuild/apply-dictionaries-overrides.php:/data/apply-dictionaries-overrides.php - ./features:/data/features - ./tests:/data/tests + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker command: ["/data/run-tests.sh"] test-browser: @@ -89,6 +94,9 @@ services: # 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 command: /data/run-debug.sh ports: - "80:80" @@ -125,6 +133,9 @@ services: # 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 command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' ports: - "8085:80" @@ -155,6 +166,9 @@ services: # 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 command: /data/run.sh ports: - "8086:80" @@ -181,6 +195,9 @@ services: # 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 ports: - "8081:80" environment: @@ -204,6 +221,9 @@ services: # Utilize custom metadata - ./development/sp2-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + + # Local modules + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker ports: - "8082:80" environment: From bffa32104394c2612975926344cd8acc15e75db0 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:57:50 -0600 Subject: [PATCH 03/92] move Validator into lib directory to avoid need for custom autoload --- .../lib/Auth/Process/ExpiryDate.php | 16 +++------------- modules/expirychecker/{src => lib}/Validator.php | 3 ++- 2 files changed, 5 insertions(+), 14 deletions(-) rename modules/expirychecker/{src => lib}/Validator.php (97%) diff --git a/modules/expirychecker/lib/Auth/Process/ExpiryDate.php b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php index fbfd7fd4..0275dffb 100644 --- a/modules/expirychecker/lib/Auth/Process/ExpiryDate.php +++ b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php @@ -4,12 +4,12 @@ use Psr\Log\LoggerInterface; use Sil\Psr3Adapters\Psr3SamlLogger; -use Sil\SspExpiryChecker\Validator; use SimpleSAML\Auth\ProcessingChain; use SimpleSAML\Auth\ProcessingFilter; use SimpleSAML\Auth\State; use SimpleSAML\Module; use SimpleSAML\Module\expirychecker\Utilities; +use SimpleSAML\Module\expirychecker\Validator; use SimpleSAML\Session; use SimpleSAML\Utils\HTTP; @@ -45,9 +45,7 @@ class ExpiryDate extends ProcessingFilter public function __construct($config, $reserved) { parent::__construct($config, $reserved); - - $this->initComposerAutoloader(); - + assert('is_array($config)'); $this->initLogger($config); @@ -175,15 +173,7 @@ public static function skipSplashPagesFor($seconds) ); $session->save(); } - - protected function initComposerAutoloader() - { - $path = __DIR__ . '/../../../vendor/autoload.php'; - if (file_exists($path)) { - require_once $path; - } - } - + protected function initLogger($config) { $loggerClass = $config['loggerClass'] ?? Psr3SamlLogger::class; diff --git a/modules/expirychecker/src/Validator.php b/modules/expirychecker/lib/Validator.php similarity index 97% rename from modules/expirychecker/src/Validator.php rename to modules/expirychecker/lib/Validator.php index 5ee64919..ba0020ac 100644 --- a/modules/expirychecker/src/Validator.php +++ b/modules/expirychecker/lib/Validator.php @@ -1,5 +1,6 @@ Date: Mon, 15 Apr 2024 15:01:01 -0600 Subject: [PATCH 04/92] add behat tests from the expirychecker repo --- Dockerfile | 1 + behat.yml | 17 ++++++++ development/idp-local/config/authsources.php | 23 +++++++++- docker-compose.yml | 1 + dockerbuild/run-integration-tests.sh | 5 +-- features/bootstrap/ExpiryContext.php | 46 ++++++-------------- features/bootstrap/FeatureContext.php | 14 +++--- features/expirychecker.feature | 33 +++++++++++++- 8 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 behat.yml diff --git a/Dockerfile b/Dockerfile index 1c701fdb..e843d3ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,6 +43,7 @@ ENV SSP_PATH /data/vendor/simplesamlphp/simplesamlphp # Copy modules into simplesamlphp COPY modules/ $SSP_PATH/modules +COPY behat.yml . # Copy in SSP override files RUN mv $SSP_PATH/www/index.php $SSP_PATH/www/ssp-index.php diff --git a/behat.yml b/behat.yml new file mode 100644 index 00000000..9c205e35 --- /dev/null +++ b/behat.yml @@ -0,0 +1,17 @@ +default: + suites: + dictionary_features: + paths: [ '%paths.base%//features//dictionary-overrides.feature' ] + contexts: [ 'FeatureContext' ] + expiry_features: + paths: [ '%paths.base%//features//expirychecker.feature' ] + contexts: [ 'ExpiryContext' ] + material_features: + paths: [ '%paths.base%//features//material.feature' ] + contexts: [ 'FeatureContext' ] + mfa_features: + paths: [ '%paths.base%//features//mfa.feature' ] + contexts: [ 'FeatureContext' ] + profilereview_features: + paths: [ '%paths.base%//features//profilereview.feature' ] + contexts: [ 'FeatureContext' ] diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 3424be4b..c8b786ee 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -24,7 +24,7 @@ gmdate('YmdHis\Z', strtotime('+6 months')), // Distant future ], ], - 'near_future:a' => [ + 'near_future:b' => [ 'eduPersonPrincipalName' => ['NEAR_FUTURE@ssp-idp1.local'], 'sn' => ['Future'], 'givenName' => ['Near'], @@ -35,7 +35,7 @@ gmdate('YmdHis\Z', strtotime('+1 day')), // Very soon ], ], - 'already_past:a' => [ + 'already_past:c' => [ 'eduPersonPrincipalName' => ['ALREADY_PAST@ssp-idp1.local'], 'sn' => ['Past'], 'givenName' => ['Already'], @@ -46,5 +46,24 @@ gmdate('YmdHis\Z', strtotime('-1 day')), // In the past ], ], + 'missing_exp:d' => [ + 'eduPersonPrincipalName' => ['MISSING_EXP@ssp-idp-1.local'], + 'sn' => ['Expiration'], + 'givenName' => ['Missing'], + 'mail' => ['missing_exp@example.com'], + 'employeeNumber' => ['44444'], + 'cn' => ['MISSING_EXP'], + ], + 'invalid_exp:e' => [ + 'eduPersonPrincipalName' => ['INVALID_EXP@ssp-idp-1.local'], + 'sn' => ['Expiration'], + 'givenName' => ['Invalid'], + 'mail' => ['invalid_exp@example.com'], + 'employeeNumber' => ['55555'], + 'cn' => ['INVALID_EXP'], + 'schacExpiryDate' => [ + 'invalid' + ], + ], ], ]; diff --git a/docker-compose.yml b/docker-compose.yml index 2f0b2267..084d4391 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,7 @@ services: - ./dockerbuild/run-tests.sh:/data/run-tests.sh - ./dockerbuild/apply-dictionaries-overrides.php:/data/apply-dictionaries-overrides.php - ./features:/data/features + - ./behat.yml:/data/behat.yml - ./tests:/data/tests - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker command: ["/data/run-tests.sh"] diff --git a/dockerbuild/run-integration-tests.sh b/dockerbuild/run-integration-tests.sh index d57f8b27..c46b02c5 100755 --- a/dockerbuild/run-integration-tests.sh +++ b/dockerbuild/run-integration-tests.sh @@ -11,8 +11,5 @@ whenavail "ssp-idp1.local" 80 10 echo IDP 1 ready whenavail "ssp-sp1.local" 80 10 echo SP 1 ready ./vendor/bin/behat \ - --append-snippets \ - --snippets-for=FeatureContext \ --no-interaction \ - --stop-on-failure #\ - #--strict + --stop-on-failure diff --git a/features/bootstrap/ExpiryContext.php b/features/bootstrap/ExpiryContext.php index 5017ba12..68bc5d79 100644 --- a/features/bootstrap/ExpiryContext.php +++ b/features/bootstrap/ExpiryContext.php @@ -1,8 +1,4 @@ session = new Session($driver); - $this->session->start(); - } - + + /** * Assert that the given page has a form that contains the given text. * @@ -58,7 +41,7 @@ protected function assertFormContains($text, $page) $page->getHtml() )); } - + /** * Assert that the given page does NOT have a form that contains the given * text. @@ -80,7 +63,7 @@ protected function assertFormNotContains($text, $page) } } } - + /** * Get the login button from the given page. * @@ -117,11 +100,9 @@ public function iProvideCredentialsThatWillExpireInTheDistantFuture() */ public function iLogin() { - $this->session->visit('http://sp/module.php/core/authenticate.php?as=ssp-hub-idp'); - $page = $this->session->getPage(); - $page->fillField('username', $this->username); - $page->fillField('password', $this->password); - $this->submitLoginForm($page); + $this->fillField('username', $this->username); + $this->fillField('password', $this->password); + $this->pressButton('Login'); } /** @@ -129,8 +110,7 @@ public function iLogin() */ public function iShouldEndUpAtMyIntendedDestination() { - $page = $this->session->getPage(); - Assert::assertContains('Your attributes', $page->getHtml()); + $this->assertPageBodyContainsText('Your attributes'); } /** @@ -151,7 +131,7 @@ public function iShouldSeeAWarningThatMyPasswordWillExpireSoon() $page = $this->session->getPage(); Assert::assertContains('will expire', $page->getHtml()); } - + /** * Submit the login form, including the secondary page's form (if * simpleSAMLphp shows another page because JavaScript isn't supported). @@ -162,13 +142,13 @@ protected function submitLoginForm($page) { $loginButton = $this->getLoginButton($page); $loginButton->click(); - + // SimpleSAMLphp 1.15 markup for secondary page: $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); if ($postLoginSubmitButton instanceof NodeElement) { $postLoginSubmitButton->click(); } else { - + // SimpleSAMLphp 1.14 markup for secondary page: $body = $page->find('css', 'body'); if ($body instanceof NodeElement) { diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index bc5d23ca..603cfc9e 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -14,10 +14,10 @@ class FeatureContext extends MinkContext private const HUB_BAD_AUTH_SOURCE_URL = 'http://ssp-hub.local/module.php/core/authenticate.php?as=wrong'; 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'; - private const SP1_LOGIN_PAGE = 'http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub'; + protected const SP1_LOGIN_PAGE = 'http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub'; /** @var Session */ - private $session; + protected $session; public function __construct() { @@ -161,7 +161,7 @@ public function iShouldSeeAPageIndicatingThatISuccessfullyLoggedIn() $this->assertPageBodyContainsText('Your attributes'); } - private function assertPageBodyContainsText(string $expectedText) + protected function assertPageBodyContainsText(string $expectedText) { $page = $this->session->getPage(); $body = $page->find('css', 'body'); @@ -169,11 +169,11 @@ private function assertPageBodyContainsText(string $expectedText) } /** - * @When I log in as a user who's password is about to expire + * @When I log in as a user whose password is about to expire */ public function iLogInAsAUserWhosPasswordIsAboutToExpire() { - $this->logInAs('near_future', 'a'); + $this->logInAs('near_future', 'b'); } /** @@ -185,11 +185,11 @@ public function iShouldSeeAPageWarningMeThatMyPasswordIsAboutToExpire() } /** - * @When I log in as a user who's password has expired + * @When I log in as a user whose password has expired */ public function iLogInAsAUserWhosPasswordHasExpired() { - $this->logInAs('already_past', 'a'); + $this->logInAs('already_past', 'c'); } /** diff --git a/features/expirychecker.feature b/features/expirychecker.feature index 80a9d51c..503cb391 100644 --- a/features/expirychecker.feature +++ b/features/expirychecker.feature @@ -8,9 +8,38 @@ Feature: Expiry Checker module Then I should see a page indicating that I successfully logged in Scenario: Password is about to expire - When I log in as a user who's password is about to expire + When I log in as a user whose password is about to expire Then I should see a page warning me that my password is about to expire Scenario: Password has expired - When I log in as a user who's password has expired + When I log in as a user whose password has expired Then I should see a page telling me that my password has expired + + Scenario: Password will expire in the distant future + Given I provide credentials that will expire in the distant future + When I login + Then I should end up at my intended destination + + Scenario: Password will expire tomorrow + Given I provide credentials that will expire very soon + When I login + Then I should see a warning that my password will expire soon + And there should be a way to go change my password now + And there should be a way to continue without changing my password + + Scenario: Password has expired + Given I provide credentials that have expired + When I login + Then I should see a message that my password has expired + And there should be a way to go change my password now + But there should NOT be a way to continue without changing my password + + Scenario: Reject missing expiration date + Given I provide credentials that have no password expiration date + When I login + Then I should see an error message + + Scenario: Reject invalid expiration date + Given I provide credentials that have an invalid password expiration date + When I login + Then I should see an error message From bc493f0a24b45375c7cfb9d658f018e5d2a5ad1c Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:05:37 -0600 Subject: [PATCH 05/92] remove redundant test scenarios --- features/bootstrap/FeatureContext.php | 49 --------------------------- features/expirychecker.feature | 12 ------- 2 files changed, 61 deletions(-) diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 603cfc9e..93f39503 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -144,23 +144,6 @@ public function iGoToTheSp1LoginPage() $this->visit(self::SP1_LOGIN_PAGE); } - /** - * @When I log in as a user who's password is NOT about to expire - */ - public function iLogInAsAUserWhosPasswordIsNotAboutToExpire() - { - $this->logInAs('distant_future', 'a'); - } - - /** - * @Then I should see a page indicating that I successfully logged in - */ - public function iShouldSeeAPageIndicatingThatISuccessfullyLoggedIn() - { - $this->assertResponseStatus(200); - $this->assertPageBodyContainsText('Your attributes'); - } - protected function assertPageBodyContainsText(string $expectedText) { $page = $this->session->getPage(); @@ -168,38 +151,6 @@ protected function assertPageBodyContainsText(string $expectedText) Assert::contains($body->getText(), $expectedText); } - /** - * @When I log in as a user whose password is about to expire - */ - public function iLogInAsAUserWhosPasswordIsAboutToExpire() - { - $this->logInAs('near_future', 'b'); - } - - /** - * @Then I should see a page warning me that my password is about to expire - */ - public function iShouldSeeAPageWarningMeThatMyPasswordIsAboutToExpire() - { - $this->assertPageBodyContainsText('Password expiring soon'); - } - - /** - * @When I log in as a user whose password has expired - */ - public function iLogInAsAUserWhosPasswordHasExpired() - { - $this->logInAs('already_past', 'c'); - } - - /** - * @Then I should see a page telling me that my password has expired - */ - public function iShouldSeeAPageTellingMeThatMyPasswordHasExpired() - { - $this->assertPageBodyContainsText('Your password has expired'); - } - private static function ensureFolderExistsForTestFile($filePath) { $folder = dirname($filePath); diff --git a/features/expirychecker.feature b/features/expirychecker.feature index 503cb391..c690015c 100644 --- a/features/expirychecker.feature +++ b/features/expirychecker.feature @@ -2,18 +2,6 @@ Feature: Expiry Checker module Background: Given I go to the SP1 login page And I click on the "IDP 1" tile - - Scenario: Password is not about to expire - When I log in as a user who's password is NOT about to expire - Then I should see a page indicating that I successfully logged in - - Scenario: Password is about to expire - When I log in as a user whose password is about to expire - Then I should see a page warning me that my password is about to expire - - Scenario: Password has expired - When I log in as a user whose password has expired - Then I should see a page telling me that my password has expired Scenario: Password will expire in the distant future Given I provide credentials that will expire in the distant future From 7d63faee64b2bb66ffb5f54f0486188e31756990 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:17:59 -0600 Subject: [PATCH 06/92] remove behat.yml from Docker image and add it to actions-services.yml --- Dockerfile | 1 - actions-services.yml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e843d3ad..1c701fdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,6 @@ ENV SSP_PATH /data/vendor/simplesamlphp/simplesamlphp # Copy modules into simplesamlphp COPY modules/ $SSP_PATH/modules -COPY behat.yml . # Copy in SSP override files RUN mv $SSP_PATH/www/index.php $SSP_PATH/www/ssp-index.php diff --git a/actions-services.yml b/actions-services.yml index 72d4851f..48a1d348 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -12,6 +12,7 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh - ./dockerbuild/run-tests.sh:/data/run-tests.sh - ./features:/data/features + - ./behat.yml:/data/behat.yml - ./tests:/data/tests test-browser: From 9057509077a19a9c6cc9ccc6edd13156edd418a7 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:16:20 -0600 Subject: [PATCH 07/92] copy content of the profilereview module https://github.com/silinternational/simplesamlphp-module-profilereview --- .../lib/Auth/Process/ProfileReview.php | 350 ++++++++++++++++++ modules/profilereview/src/Assert.php | 57 +++ modules/profilereview/src/LoggerFactory.php | 41 ++ .../templates/nag-for-method.php | 21 ++ .../profilereview/templates/nag-for-mfa.php | 28 ++ modules/profilereview/templates/review.php | 62 ++++ modules/profilereview/www/nag.php | 45 +++ modules/profilereview/www/review.php | 47 +++ 8 files changed, 651 insertions(+) create mode 100644 modules/profilereview/lib/Auth/Process/ProfileReview.php create mode 100644 modules/profilereview/src/Assert.php create mode 100644 modules/profilereview/src/LoggerFactory.php create mode 100644 modules/profilereview/templates/nag-for-method.php create mode 100644 modules/profilereview/templates/nag-for-mfa.php create mode 100644 modules/profilereview/templates/review.php create mode 100644 modules/profilereview/www/nag.php create mode 100644 modules/profilereview/www/review.php diff --git a/modules/profilereview/lib/Auth/Process/ProfileReview.php b/modules/profilereview/lib/Auth/Process/ProfileReview.php new file mode 100644 index 00000000..cc1377b1 --- /dev/null +++ b/modules/profilereview/lib/Auth/Process/ProfileReview.php @@ -0,0 +1,350 @@ +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'], + ])); + + 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/src/Assert.php b/modules/profilereview/src/Assert.php new file mode 100644 index 00000000..1660cf59 --- /dev/null +++ b/modules/profilereview/src/Assert.php @@ -0,0 +1,57 @@ +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..bcb6d87d --- /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..40173a18 --- /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'], +])); From d9c0772fc8b3aa84f7f7c40e6c3c60cea3b16458 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:18:46 -0600 Subject: [PATCH 08/92] use the local profilereview code and remove from composer --- composer.json | 1 - composer.lock | 55 +--------------------------------------------- docker-compose.yml | 7 ++++++ 3 files changed, 8 insertions(+), 55 deletions(-) diff --git a/composer.json b/composer.json index da75894e..b18b9ec6 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,6 @@ "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", diff --git a/composer.lock b/composer.lock index 1eb2f63e..6fa3db8e 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": "301cb3c821e2c235e4429601cb704258", "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/docker-compose.yml b/docker-compose.yml index 084d4391..10432450 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: @@ -52,6 +53,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 +100,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" @@ -137,6 +140,7 @@ services: # 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" @@ -170,6 +174,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" @@ -199,6 +204,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 +231,7 @@ services: # Local modules - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview ports: - "8082:80" environment: From d446c848f157ea71d631076dc8df24b1638fafc3 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:45:35 -0600 Subject: [PATCH 09/92] move Assert and LoggerFactory into lib directory and change namespace --- composer.json | 1 + composer.lock | 2 +- modules/profilereview/{src => lib}/Assert.php | 4 +--- modules/profilereview/lib/Auth/Process/ProfileReview.php | 2 +- modules/profilereview/{src => lib}/LoggerFactory.php | 6 ++---- modules/profilereview/www/nag.php | 2 +- modules/profilereview/www/review.php | 4 ++-- 7 files changed, 9 insertions(+), 12 deletions(-) rename modules/profilereview/{src => lib}/Assert.php (96%) rename modules/profilereview/{src => lib}/LoggerFactory.php (89%) diff --git a/composer.json b/composer.json index b18b9ec6..fa3b6c49 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "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 6fa3db8e..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": "301cb3c821e2c235e4429601cb704258", + "content-hash": "b4f4532a5a284c0b780aaa48860974ae", "packages": [ { "name": "aws/aws-crt-php", diff --git a/modules/profilereview/src/Assert.php b/modules/profilereview/lib/Assert.php similarity index 96% rename from modules/profilereview/src/Assert.php rename to modules/profilereview/lib/Assert.php index 1660cf59..f20dff99 100644 --- a/modules/profilereview/src/Assert.php +++ b/modules/profilereview/lib/Assert.php @@ -1,7 +1,5 @@ Date: Tue, 16 Apr 2024 14:51:37 -0600 Subject: [PATCH 10/92] add behat tests from the profilereview repo --- behat.yml | 2 +- features/bootstrap/ProfileReviewContext.php | 303 ++++++++++++++++++++ features/profilereview.feature | 56 +++- 3 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 features/bootstrap/ProfileReviewContext.php 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/features/bootstrap/ProfileReviewContext.php b/features/bootstrap/ProfileReviewContext.php new file mode 100644 index 00000000..6555f51b --- /dev/null +++ b/features/bootstrap/ProfileReviewContext.php @@ -0,0 +1,303 @@ +driver = new GoutteDriver(); + $this->session = new Session($this->driver); + $this->session->start(); + } + + /** + * Assert that the given page has a form that contains the given text. + * + * @param string $text The text (or HTML) to search for. + * @param DocumentElement $page The page to search in. + * @return void + */ + protected function assertFormContains($text, $page) + { + $forms = $page->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() + { + $this->session->visit($this->nonPwManagerUrl); + $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 = 'a'; + } + + /** + * @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; + $this->password = 'a'; + } + + /** + * @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; + } + + protected function clickLink($text) + { + $this->session->getPage()->clickLink($text); + } + + /** + * @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 = 'a'; + } + + /** + * @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/profilereview.feature b/features/profilereview.feature index dbb843e5..9f147a6e 100644 --- a/features/profilereview.feature +++ b/features/profilereview.feature @@ -1,7 +1,49 @@ -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 + + 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 + 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 From be690536e40b21f19ec161ffaace4f8c7de11323 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:16:03 -0600 Subject: [PATCH 11/92] add profilereview users into authsources.php --- development/idp-local/config/authsources.php | 122 +++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index c8b786ee..97f0fc8b 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -65,5 +65,127 @@ 'invalid' ], ], + 'no_review:a' => [ + 'eduPersonPrincipalName' => ['NO_REVIEW@idp'], + 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], + 'sn' => ['Review'], + 'givenName' => ['No'], + 'mail' => ['no_review@example.com'], + 'employeeNumber' => ['11111'], + 'cn' => ['NO_REVIEW'], + '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:a' => [ + 'eduPersonPrincipalName' => ['MFA_ADD@idp'], + 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], + 'sn' => ['Add'], + 'givenName' => ['Mfa'], + 'mail' => ['mfa_add@example.com'], + 'employeeNumber' => ['22222'], + 'cn' => ['MFA_ADD'], + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'yes', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + ], + 'profile_review' => 'no' + ], + 'method_add:a' => [ + 'eduPersonPrincipalName' => ['METHOD_ADD@methodidp'], + 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], + 'sn' => ['Add'], + 'givenName' => ['Method'], + 'mail' => ['method_add@example.com'], + 'employeeNumber' => ['44444'], + 'cn' => ['METHOD_ADD'], + '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:a' => [ + 'eduPersonPrincipalName' => ['METHOD_REVIEW@methodidp'], + 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], + 'sn' => ['Review'], + 'givenName' => ['Method'], + 'mail' => ['method_review@example.com'], + 'employeeNumber' => ['55555'], + 'cn' => ['METHOD_REVIEW'], + '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' + ], ], ]; From ccb25b01d456ea374668cf8a5038ffec19f758d3 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:59:18 -0600 Subject: [PATCH 12/92] fix profilereview tests to run correctly here except the ones that look for specific strings fail because of the material theme --- actions-services.yml | 7 ++ development/idp-local/UserPass.php | 91 +++++++++++++++++++ development/idp-local/config/authsources.php | 12 +++ .../idp-local/metadata/saml20-idp-hosted.php | 17 +++- docker-compose.yml | 6 ++ features/bootstrap/ProfileReviewContext.php | 43 +-------- features/profilereview.feature | 6 ++ .../lib/Auth/Process/ProfileReview.php | 1 + 8 files changed, 140 insertions(+), 43 deletions(-) create mode 100644 development/idp-local/UserPass.php diff --git a/actions-services.yml b/actions-services.yml index 48a1d348..ded359e7 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -7,6 +7,8 @@ services: - ssp-idp1.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 +69,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 +80,8 @@ 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: "material:material" 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 97f0fc8b..71cb48bf 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -73,6 +73,9 @@ 'mail' => ['no_review@example.com'], 'employeeNumber' => ['11111'], 'cn' => ['NO_REVIEW'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], 'mfa' => [ 'prompt' => 'yes', 'add' => 'no', @@ -102,6 +105,9 @@ 'mail' => ['mfa_add@example.com'], 'employeeNumber' => ['22222'], 'cn' => ['MFA_ADD'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], 'mfa' => [ 'prompt' => 'no', 'add' => 'yes', @@ -120,6 +126,9 @@ 'mail' => ['method_add@example.com'], 'employeeNumber' => ['44444'], 'cn' => ['METHOD_ADD'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], 'mfa' => [ 'prompt' => 'yes', 'add' => 'no', @@ -149,6 +158,9 @@ 'mail' => ['method_review@example.com'], 'employeeNumber' => ['55555'], 'cn' => ['METHOD_REVIEW'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], 'mfa' => [ 'prompt' => 'yes', 'add' => 'no', 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/docker-compose.yml b/docker-compose.yml index 10432450..5c39b727 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,7 @@ services: - 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 @@ -135,6 +136,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 @@ -149,6 +153,8 @@ 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" diff --git a/features/bootstrap/ProfileReviewContext.php b/features/bootstrap/ProfileReviewContext.php index 6555f51b..2d0caaee 100644 --- a/features/bootstrap/ProfileReviewContext.php +++ b/features/bootstrap/ProfileReviewContext.php @@ -1,53 +1,20 @@ driver = new GoutteDriver(); - $this->session = new Session($this->driver); - $this->session->start(); - } - + /** * Assert that the given page has a form that contains the given text. * @@ -96,7 +63,6 @@ protected function getLoginButton($page) */ public function iLogin() { - $this->session->visit($this->nonPwManagerUrl); $page = $this->session->getPage(); try { $page->fillField('username', $this->username); @@ -217,11 +183,6 @@ protected function pageContainsElementWithText($cssSelector, $text) } return false; } - - protected function clickLink($text) - { - $this->session->getPage()->clickLink($text); - } /** * @Then there should be a way to continue to my intended destination diff --git a/features/profilereview.feature b/features/profilereview.feature index 9f147a6e..56643e9d 100644 --- a/features/profilereview.feature +++ b/features/profilereview.feature @@ -1,4 +1,7 @@ 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 @@ -22,6 +25,9 @@ Feature: Prompt to review profile information 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: diff --git a/modules/profilereview/lib/Auth/Process/ProfileReview.php b/modules/profilereview/lib/Auth/Process/ProfileReview.php index 3ad70758..8b8584a9 100644 --- a/modules/profilereview/lib/Auth/Process/ProfileReview.php +++ b/modules/profilereview/lib/Auth/Process/ProfileReview.php @@ -206,6 +206,7 @@ public static function redirectToProfile(&$state) 'module' => 'profilereview', 'event' => 'redirect to profile', 'employeeId' => $state['employeeId'], + 'profileUrl' => $profileUrl, ])); HTTP::redirectTrustedURL($profileUrl); From 81d06fd70e56fa15c4abf69bb2eb2aac516926bf Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:59:42 -0600 Subject: [PATCH 13/92] enable test IDP2; use "default" theme on IDP1 and "material" on IDP2 --- development/hub/metadata/idp-remote.php | 4 +- development/idp2-local/config/config.php | 1153 ++++++++++++----- .../idp2-local/metadata/saml20-idp-hosted.php | 4 + docker-compose.yml | 5 +- features/bootstrap/ExpiryContext.php | 2 +- features/bootstrap/FeatureContext.php | 18 +- features/material.feature | 4 +- 7 files changed, 854 insertions(+), 336 deletions(-) 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/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 5c39b727..2eb456c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -157,14 +157,14 @@ services: 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 @@ -191,6 +191,7 @@ services: IDP_NAME: "IDP 2" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" + THEME_USE: "material:material" ssp-sp1.local: build: . 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/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 From c8908fd7df8067eb5cb13d11c117754f44e54de9 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:15:53 -0600 Subject: [PATCH 14/92] update actions-services.yml with test IDP changes --- actions-services.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/actions-services.yml b/actions-services.yml index ded359e7..5ff3ccbd 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -84,6 +84,35 @@ services: 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" ssp-sp1.local: From ad4885495ab46682cbf3489920aa0c1c94a1a1be Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:23:02 -0600 Subject: [PATCH 15/92] add ssp-idp2.local to depends_on --- actions-services.yml | 1 + docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/actions-services.yml b/actions-services.yml index 5ff3ccbd..0fc10d60 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -5,6 +5,7 @@ services: depends_on: - ssp-hub.local - ssp-idp1.local + - ssp-idp2.local - ssp-sp1.local - test-browser environment: diff --git a/docker-compose.yml b/docker-compose.yml index 2eb456c7..cecf2a4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: depends_on: - ssp-hub.local - ssp-idp1.local + - ssp-idp2.local - ssp-sp1.local - test-browser environment: From 1e694e964949fe4e15fee7049ab986849308ba86 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 3 May 2024 09:01:32 +0800 Subject: [PATCH 16/92] change expirychecker namespace to ExpiryChecker for aesthetics --- modules/expirychecker/lib/Auth/Process/ExpiryDate.php | 6 +++--- modules/expirychecker/lib/Utilities.php | 2 +- modules/expirychecker/lib/Validator.php | 2 +- modules/expirychecker/www/about2expire.php | 4 ++-- modules/expirychecker/www/expired.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/expirychecker/lib/Auth/Process/ExpiryDate.php b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php index 0275dffb..d4d75c65 100644 --- a/modules/expirychecker/lib/Auth/Process/ExpiryDate.php +++ b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php @@ -1,6 +1,6 @@ Date: Fri, 3 May 2024 09:04:35 +0800 Subject: [PATCH 17/92] Revert "change expirychecker namespace to ExpiryChecker for aesthetics" This reverts commit 1e694e964949fe4e15fee7049ab986849308ba86. --- modules/expirychecker/lib/Auth/Process/ExpiryDate.php | 6 +++--- modules/expirychecker/lib/Utilities.php | 2 +- modules/expirychecker/lib/Validator.php | 2 +- modules/expirychecker/www/about2expire.php | 4 ++-- modules/expirychecker/www/expired.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/expirychecker/lib/Auth/Process/ExpiryDate.php b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php index d4d75c65..0275dffb 100644 --- a/modules/expirychecker/lib/Auth/Process/ExpiryDate.php +++ b/modules/expirychecker/lib/Auth/Process/ExpiryDate.php @@ -1,6 +1,6 @@ Date: Tue, 7 May 2024 11:07:18 +0800 Subject: [PATCH 18/92] use a different password for each profilereview user --- development/idp-local/config/authsources.php | 8 ++++---- features/bootstrap/ProfileReviewContext.php | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 71cb48bf..f9fa9ee5 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -65,7 +65,7 @@ 'invalid' ], ], - 'no_review:a' => [ + 'no_review:e' => [ 'eduPersonPrincipalName' => ['NO_REVIEW@idp'], 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], 'sn' => ['Review'], @@ -97,7 +97,7 @@ ], 'profile_review' => 'no' ], - 'mfa_add:a' => [ + 'mfa_add:f' => [ 'eduPersonPrincipalName' => ['MFA_ADD@idp'], 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], 'sn' => ['Add'], @@ -118,7 +118,7 @@ ], 'profile_review' => 'no' ], - 'method_add:a' => [ + 'method_add:g' => [ 'eduPersonPrincipalName' => ['METHOD_ADD@methodidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], 'sn' => ['Add'], @@ -150,7 +150,7 @@ ], 'profile_review' => 'no' ], - 'profile_review:a' => [ + 'profile_review:h' => [ 'eduPersonPrincipalName' => ['METHOD_REVIEW@methodidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], 'sn' => ['Review'], diff --git a/features/bootstrap/ProfileReviewContext.php b/features/bootstrap/ProfileReviewContext.php index 2d0caaee..8e60c0d3 100644 --- a/features/bootstrap/ProfileReviewContext.php +++ b/features/bootstrap/ProfileReviewContext.php @@ -151,7 +151,7 @@ public function iProvideCredentialsThatDoNotNeedReview() { // See `development/idp-local/config/authsources.php` for options. $this->username = 'no_review'; - $this->password = 'a'; + $this->password = 'e'; } /** @@ -161,7 +161,19 @@ public function iProvideCredentialsThatAreDueForAReminder($category, $nagType) { // See `development/idp-local/config/authsources.php` for options. $this->username = $category . '_' . $nagType; - $this->password = 'a'; + switch ($this->username) { + case 'mfa_add': + $this->password = 'f'; + break; + + case 'method_add': + $this->password = 'g'; + break; + + case 'profile_review': + $this->password = 'h'; + break; + } } /** @@ -249,7 +261,7 @@ public function iProvideCredentialsForAUserThatHasUsedTheManagerMfaOption() { // See `development/idp-local/config/authsources.php` for options. $this->username = 'profile_review'; - $this->password = 'a'; + $this->password = 'h'; } /** From 04c34b1a8b901969b3f8f7fb82cd03d899cede84 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 7 May 2024 15:45:55 +0800 Subject: [PATCH 19/92] add module variables to local.env.dist file [skip ci] --- local.env.dist | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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= From 09380a65677ac27eb5acf32799852a23247b1877 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 7 May 2024 15:48:21 +0800 Subject: [PATCH 20/92] copy content from the mfa module repo https://github.com/silinternational/simplesamlphp-module-mfa --- features/bootstrap/context/MfaContext.php | 845 ++++++++++++++++ features/fakes/FakeIdBrokerClient.php | 136 +++ features/mfa.feature | 262 ++++- local.env.dist | 6 + modules/mfa/lib/Auth/Process/Mfa.php | 956 ++++++++++++++++++ modules/mfa/src/Assert.php | 57 ++ modules/mfa/src/LoggerFactory.php | 41 + modules/mfa/src/LoginBrowser.php | 41 + modules/mfa/templates/low-on-backup-codes.php | 21 + modules/mfa/templates/must-set-up-mfa.php | 16 + modules/mfa/templates/new-backup-codes.php | 40 + modules/mfa/templates/out-of-backup-codes.php | 37 + .../templates/prompt-for-mfa-backupcode.php | 57 ++ .../mfa/templates/prompt-for-mfa-manager.php | 49 + modules/mfa/templates/prompt-for-mfa-totp.php | 53 + .../mfa/templates/prompt-for-mfa-webauthn.php | 79 ++ modules/mfa/templates/send-manager-mfa.php | 20 + modules/mfa/www/low-on-backup-codes.php | 40 + modules/mfa/www/must-set-up-mfa.php | 32 + modules/mfa/www/new-backup-codes.php | 39 + modules/mfa/www/out-of-backup-codes.php | 42 + modules/mfa/www/prompt-for-mfa.php | 114 +++ modules/mfa/www/send-manager-mfa.php | 45 + modules/mfa/www/simplewebauthn/LICENSE.md | 21 + modules/mfa/www/simplewebauthn/browser.js | 2 + 25 files changed, 3030 insertions(+), 21 deletions(-) create mode 100644 features/bootstrap/context/MfaContext.php create mode 100644 features/fakes/FakeIdBrokerClient.php create mode 100644 modules/mfa/lib/Auth/Process/Mfa.php create mode 100644 modules/mfa/src/Assert.php create mode 100644 modules/mfa/src/LoggerFactory.php create mode 100644 modules/mfa/src/LoginBrowser.php create mode 100644 modules/mfa/templates/low-on-backup-codes.php create mode 100644 modules/mfa/templates/must-set-up-mfa.php create mode 100644 modules/mfa/templates/new-backup-codes.php create mode 100644 modules/mfa/templates/out-of-backup-codes.php create mode 100644 modules/mfa/templates/prompt-for-mfa-backupcode.php create mode 100644 modules/mfa/templates/prompt-for-mfa-manager.php create mode 100644 modules/mfa/templates/prompt-for-mfa-totp.php create mode 100644 modules/mfa/templates/prompt-for-mfa-webauthn.php create mode 100644 modules/mfa/templates/send-manager-mfa.php create mode 100644 modules/mfa/www/low-on-backup-codes.php create mode 100644 modules/mfa/www/must-set-up-mfa.php create mode 100644 modules/mfa/www/new-backup-codes.php create mode 100644 modules/mfa/www/out-of-backup-codes.php create mode 100644 modules/mfa/www/prompt-for-mfa.php create mode 100644 modules/mfa/www/send-manager-mfa.php create mode 100644 modules/mfa/www/simplewebauthn/LICENSE.md create mode 100644 modules/mfa/www/simplewebauthn/browser.js diff --git a/features/bootstrap/context/MfaContext.php b/features/bootstrap/context/MfaContext.php new file mode 100644 index 00000000..020c49e8 --- /dev/null +++ b/features/bootstrap/context/MfaContext.php @@ -0,0 +1,845 @@ +driver = new GoutteDriver(); + $this->session = new Session($this->driver); + $this->session->start(); + } + + /** + * Assert that the given page has a form that contains the given text. + * + * @param string $text The text (or HTML) to search for. + * @param DocumentElement $page The page to search in. + * @return void + */ + protected function assertFormContains($text, $page) + { + $forms = $page->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 "continue" button. + * + * @param DocumentElement $page The page. + * @return NodeElement + */ + protected function getContinueButton($page) + { + $continueButton = $page->find('css', '[name=continue]'); + return $continueButton; + } + + /** + * Get the login button from the given page. + * + * @param DocumentElement $page The page. + * @return NodeElement + */ + protected function getLoginButton($page) + { + $buttons = $page->findAll('css', 'button'); + $loginButton = null; + foreach ($buttons as $button) { + $lcButtonText = strtolower($button->getText()); + if (strpos($lcButtonText, 'login') !== false) { + $loginButton = $button; + break; + } + } + Assert::assertNotNull($loginButton, 'Failed to find the login button'); + return $loginButton; + } + + /** + * Get the button for submitting the MFA form. + * + * @param DocumentElement $page The page. + * @return NodeElement + */ + protected function getSubmitMfaButton($page) + { + $submitMfaButton = $page->find('css', '[name=submitMfa]'); + Assert::assertNotNull($submitMfaButton, 'Failed to find the submit-MFA button'); + return $submitMfaButton; + } + + /** + * @When I login + */ + public function iLogin() + { + $this->session->visit($this->nonPwManagerUrl); + $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 MFA form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitMfaForm($page) + { + $submitMfaButton = $this->getSubmitMfaButton($page); + $submitMfaButton->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 MFA + */ + public function iProvideCredentialsThatDoNotNeedMfa() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'no_mfa_needed'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that need MFA but have no MFA options available + */ + public function iProvideCredentialsThatNeedMfaButHaveNoMfaOptionsAvailable() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'must_set_up_mfa'; + $this->password = 'a'; + } + + /** + * @Then I should see a message that I have to set up MFA + */ + public function iShouldSeeAMessageThatIHaveToSetUpMfa() + { + $page = $this->session->getPage(); + Assert::assertContains('must set up 2-', $page->getHtml()); + } + + /** + * @Then there should be a way to go set up MFA now + */ + public function thereShouldBeAWayToGoSetUpMfaNow() + { + $page = $this->session->getPage(); + $this->assertFormContains('name="setUpMfa"', $page); + } + + /** + * @Given I provide credentials that need MFA and have backup codes available + */ + public function iProvideCredentialsThatNeedMfaAndHaveBackupCodesAvailable() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_backupcode'; + $this->password = 'a'; + } + + /** + * @Then I should see a prompt for a backup code + */ + public function iShouldSeeAPromptForABackupCode() + { + $page = $this->session->getPage(); + $pageHtml = $page->getHtml(); + Assert::assertContains('

Printable Backup Code

', $pageHtml); + Assert::assertContains('Enter code', $pageHtml); + } + + /** + * @Given I provide credentials that need MFA and have TOTP available + */ + public function iProvideCredentialsThatNeedMfaAndHaveTotpAvailable() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_totp'; + $this->password = 'a'; + } + + /** + * @Then I should see a prompt for a TOTP (code) + */ + public function iShouldSeeAPromptForATotpCode() + { + $page = $this->session->getPage(); + $pageHtml = $page->getHtml(); + Assert::assertContains('

Smartphone App

', $pageHtml); + Assert::assertContains('Enter 6-digit code', $pageHtml); + } + + /** + * @Given I provide credentials that need MFA and have WebAuthn available + */ + public function iProvideCredentialsThatNeedMfaAndHaveUfAvailable() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_webauthn'; + $this->password = 'a'; + } + + /** + * @Then I should see a prompt for a WebAuthn (security key) + */ + public function iShouldSeeAPromptForAWebAuthn() + { + $page = $this->session->getPage(); + Assert::assertContains('

USB Security Key

', $page->getHtml()); + } + + /** + * @Given I have logged in (again) + */ + public function iHaveLoggedIn() + { + $this->iLogin(); + } + + protected function submitMfaValue($mfaValue) + { + $page = $this->session->getPage(); + $page->fillField('mfaSubmission', $mfaValue); + $this->submitMfaForm($page); + return $page->getHtml(); + } + + /** + * @When I submit a correct backup code + */ + public function iSubmitACorrectBackupCode() + { + if (! $this->pageContainsElementWithText('h2', 'Printable Backup Code')) { + $this->clickLink('backupcode'); + } + $this->submitMfaValue(FakeIdBrokerClient::CORRECT_VALUE); + } + + 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; + } + + protected function clickLink($text) + { + $this->session->getPage()->clickLink($text); + } + + /** + * @When I submit an incorrect backup code + */ + public function iSubmitAnIncorrectBackupCode() + { + $this->submitMfaValue(FakeIdBrokerClient::INCORRECT_VALUE); + } + + /** + * @Then I should see a message that I have to wait before trying again + */ + public function iShouldSeeAMessageThatIHaveToWaitBeforeTryingAgain() + { + $page = $this->session->getPage(); + $pageHtml = $page->getHtml(); + Assert::assertContains(' wait ', $pageHtml); + Assert::assertContains('try again', $pageHtml); + } + + /** + * @Then I should see a message that it was incorrect + */ + public function iShouldSeeAMessageThatItWasIncorrect() + { + $page = $this->session->getPage(); + $pageHtml = $page->getHtml(); + Assert::assertContains('Incorrect 2-step verification code', $pageHtml); + } + + /** + * @Given I provide credentials that have a rate-limited MFA + */ + public function iProvideCredentialsThatHaveARateLimitedMfa() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_rate_limited_mfa'; + $this->password = 'a'; + } + + /** + * @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 set-up-MFA button + */ + public function iClickTheSetUpMfaButton() + { + $this->submitFormByClickingButtonNamed('setUpMfa'); + } + + /** + * @Then I should end up at the mfa-setup URL + */ + public function iShouldEndUpAtTheMfaSetupUrl() + { + $mfaSetupUrl = Env::get('MFA_SETUP_URL_FOR_TESTS'); + Assert::assertNotEmpty($mfaSetupUrl, 'No MFA_SETUP_URL_FOR_TESTS provided'); + $currentUrl = $this->session->getCurrentUrl(); + Assert::assertStringStartsWith( + $mfaSetupUrl, + $currentUrl, + 'Did NOT end up at the MFA-setup URL' + ); + } + + /** + * @Then there should NOT be a way to continue to my intended destination + */ + public function thereShouldNotBeAWayToContinueToMyIntendedDestination() + { + $page = $this->session->getPage(); + $continueButton = $this->getContinueButton($page); + Assert::assertNull($continueButton, 'Should not have found a continue button'); + } + + /** + * @Then I should NOT be able to get to my intended destination + */ + public function iShouldNotBeAbleToGetToMyIntendedDestination() + { + $this->session->visit($this->nonPwManagerUrl); + Assert::assertStringStartsNotWith( + $this->nonPwManagerUrl, + $this->session->getCurrentUrl(), + 'Failed to prevent me from getting to SPs other than the MFA setup URL' + ); + } + + /** + * @Given I provide credentials that need MFA and have 4 backup codes available + */ + public function iProvideCredentialsThatNeedMfaAndHave4BackupCodesAvailable() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_4_backupcodes'; + $this->password = 'a'; + } + + /** + * @Then I should see a message that I am running low on backup codes + */ + public function iShouldSeeAMessageThatIAmRunningLowOnBackupCodes() + { + $page = $this->session->getPage(); + Assert::assertContains( + 'You are almost out of Printable Backup Codes', + $page->getHtml() + ); + } + + /** + * @Then there should be a way to get more backup codes now + */ + public function thereShouldBeAWayToGetMoreBackupCodesNow() + { + $page = $this->session->getPage(); + $this->assertFormContains('name="getMore"', $page); + } + + /** + * @Given I provide credentials that need MFA and have 1 backup code available and no other MFA + */ + public function iProvideCredentialsThatNeedMfaAndHave1BackupCodeAvailableAndNoOtherMfa() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_1_backupcode_only'; + $this->password = 'a'; + } + + /** + * @Then I should see a message that I have used up my backup codes + */ + public function iShouldSeeAMessageThatIHaveUsedUpMyBackupCodes() + { + $page = $this->session->getPage(); + Assert::assertContains( + 'You just used your last Printable Backup Code', + $page->getHtml() + ); + } + + /** + * @Given I provide credentials that need MFA and have 1 backup code available plus some other MFA + */ + public function iProvideCredentialsThatNeedMfaAndHave1BackupCodeAvailablePlusSomeOtherMfa() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_1_backupcode_plus'; + $this->password = 'a'; + } + + /** + * @When I click the get-more-backup-codes button + */ + public function iClickTheGetMoreBackupCodesButton() + { + $this->submitFormByClickingButtonNamed('getMore'); + } + + /** + * @Then I should be told I only have :numRemaining backup codes left + */ + public function iShouldBeToldIOnlyHaveBackupCodesLeft($numRemaining) + { + $page = $this->session->getPage(); + Assert::assertContains( + 'You only have ' . $numRemaining . ' remaining', + $page->getHtml() + ); + } + + /** + * @Then I should be given more backup codes + */ + public function iShouldBeGivenMoreBackupCodes() + { + $page = $this->session->getPage(); + Assert::assertContains( + 'Here are your new Printable Backup Codes', + $page->getContent() + ); + } + + /** + * @Given I provide credentials that have WebAuthn + */ + public function iProvideCredentialsThatHaveUf() + { + $this->iProvideCredentialsThatNeedMfaAndHaveUfAvailable(); + } + + /** + * @Given the user's browser supports WebAuthn + */ + public function theUsersBrowserSupportsUf() + { + $userAgentWithWebAuthn = self::USER_AGENT_WITH_WEBAUTHN_SUPPORT; + Assert::assertTrue( + LoginBrowser::supportsWebAuthn($userAgentWithWebAuthn), + 'Update USER_AGENT_WITH_WEBAUTHN_SUPPORT to a User Agent with WebAuthn support' + ); + + $this->driver->getClient()->setServerParameter('HTTP_USER_AGENT', $userAgentWithWebAuthn); + } + + /** + * @Given I provide credentials that have WebAuthn, TOTP + */ + public function iProvideCredentialsThatHaveUfTotp() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_webauthn_totp'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that have WebAuthn, backup codes + */ + public function iProvideCredentialsThatHaveUfBackupCodes() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_webauthn_backupcodes'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that have WebAuthn, TOTP, backup codes + */ + public function iProvideCredentialsThatHaveUfTotpBackupCodes() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_webauthn_totp_backupcodes'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that have TOTP + */ + public function iProvideCredentialsThatHaveTotp() + { + $this->iProvideCredentialsThatNeedMfaAndHaveTotpAvailable(); + } + + /** + * @Given I provide credentials that have TOTP, backup codes + */ + public function iProvideCredentialsThatHaveTotpBackupCodes() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_totp_backupcodes'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that have backup codes + */ + public function iProvideCredentialsThatHaveBackupCodes() + { + $this->iProvideCredentialsThatNeedMfaAndHaveBackupCodesAvailable(); + } + + /** + * @Given I provide credentials that have a manager code, a WebAuthn and a more recently used TOTP + */ + public function IProvideCredentialsThatHaveManagerCodeWebauthnAndMoreRecentlyUsedTotp() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_mgr_code_webauthn_and_more_recently_used_totp'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that have a used WebAuthn + */ + public function IProvideCredentialsThatHaveUsedWebAuthn() + { + $this->username = 'has_webauthn_'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that have a used TOTP + */ + public function IProvideCredentialsThatHaveUsedTotp() + { + $this->username = 'has_totp_'; + $this->password = 'a'; + } + + /** + * @Given I provide credentials that have a used backup code + */ + public function IProvideCredentialsThatHaveUsedBackupCode() + { + $this->username = 'has_backup_code_'; + $this->password = 'a'; + } + + /** + * @Given and I have a more recently used TOTP + */ + public function IHaveMoreRecentlyUsedTotp() + { + $this->username .= 'and_more_recently_used_totp'; + $this->password = 'a'; + } + + /** + * @Given and I have a more recently used Webauthn + */ + public function IHaveMoreRecentlyUsedWebauthn() + { + $this->username .= 'and_more_recently_used_webauthn'; + $this->password = 'a'; + } + + /** + * @Given and I have a more recently used backup code + */ + public function IHaveMoreRecentlyUsedBackupCode() + { + $this->username .= 'and_more_recently_used_backup_code'; + $this->password = 'a'; + } + + /** + * @Given the user's browser does not support WebAuthn + */ + public function theUsersBrowserDoesNotSupportUf() + { + $userAgentWithoutWebAuthn = self::USER_AGENT_WITHOUT_WEBAUTHN_SUPPORT; + Assert::assertFalse( + LoginBrowser::supportsWebAuthn($userAgentWithoutWebAuthn), + 'Update USER_AGENT_WITHOUT_WEBAUTHN_SUPPORT to a User Agent without WebAuthn support' + ); + + $this->driver->getClient()->setServerParameter('HTTP_USER_AGENT', $userAgentWithoutWebAuthn); + } + + /** + * @Then I should not see an error message about WebAuthn being unsupported + */ + public function iShouldNotSeeAnErrorMessageAboutUfBeingUnsupported() + { + $page = $this->session->getPage(); + Assert::assertNotContains('USB Security Keys are not supported', $page->getContent()); + } + + /** + * @Then I should see an error message about WebAuthn being unsupported + */ + public function iShouldSeeAnErrorMessageAboutUfBeingUnsupported() + { + $page = $this->session->getPage(); + Assert::assertContains('USB Security Keys are not supported', $page->getContent()); + } + + /** + * @Given the user has a manager email + */ + public function theUserHasAManagerEmail() + { + $this->username .= '_and_mgr'; + } + + /** + * @Then I should see a link to send a code to the user's manager + */ + public function iShouldSeeALinkToSendACodeToTheUsersManager() + { + $page = $this->session->getPage(); + Assert::assertContains('Can\'t use any of your 2-Step Verification options', $page->getContent()); + } + + /** + * @Given the user does not have a manager email + */ + public function theUserDoesntHaveAManagerEmail() + { + /* + * No change to username needed. + */ + } + + /** + * @Then I should not see a link to send a code to the user's manager + */ + public function iShouldNotSeeALinkToSendACodeToTheUsersManager() + { + $page = $this->session->getPage(); + Assert::assertNotContains('Send a code to your manager', $page->getContent()); + } + + /** + * @When I click the Request Assistance link + */ + public function iClickTheRequestAssistanceLink() + { + $this->clickLink('Click here'); + } + + /** + * @When I click the Send a code link + */ + public function iClickTheRequestACodeLink() + { + $this->submitFormByClickingButtonNamed('send'); + } + + /** + * @Then I should see a prompt for a manager rescue code + */ + public function iShouldSeeAPromptForAManagerRescueCode() + { + $page = $this->session->getPage(); + $pageHtml = $page->getHtml(); + Assert::assertContains('

Manager Rescue Code

', $pageHtml); + Assert::assertContains('Enter code', $pageHtml); + } + + /** + * @When I submit the correct manager code + */ + public function iSubmitTheCorrectManagerCode() + { + $this->submitMfaValue(FakeIdBrokerClient::CORRECT_VALUE); + } + + /** + * @When I submit an incorrect manager code + */ + public function iSubmitAnIncorrectManagerCode() + { + $this->submitMfaValue(FakeIdBrokerClient::INCORRECT_VALUE); + } + + /** + * @Given I provide credentials that have a manager code + */ + public function iProvideCredentialsThatHaveAManagerCode() + { + // See `development/idp-local/config/authsources.php` for options. + $this->username = 'has_mgr_code'; + $this->password = 'a'; + } + + /** + * @Then there should be a way to request a manager code + */ + public function thereShouldBeAWayToRequestAManagerCode() + { + $page = $this->session->getPage(); + $this->assertFormContains('name="send"', $page); + } + + /** + * @When I click the Cancel button + */ + public function iClickTheCancelButton() + { + $this->submitFormByClickingButtonNamed('cancel'); + } +} diff --git a/features/fakes/FakeIdBrokerClient.php b/features/fakes/FakeIdBrokerClient.php new file mode 100644 index 00000000..3f339b7c --- /dev/null +++ b/features/fakes/FakeIdBrokerClient.php @@ -0,0 +1,136 @@ + $id, + 'type' => 'backupcode', + 'label' => 'Printable Codes', + 'created_utc' => '2019-01-02T03:04:05Z', + 'data' => [ + 'count' => 4, + ], + ]; + } + + /** + * Create a new MFA configuration + * @param string $employee_id + * @param string $type + * @param string $label + * @return array|null + * @throws Exception + */ + public function mfaCreate($employee_id, $type, $label = null) + { + if (empty($employee_id)) { + throw new InvalidArgumentException('employee_id is required'); + } + + if ($type === 'backupcode') { + return [ + "id" => 1234, + "data" => [ + "00000000", + "11111111", + "22222222", + "33333333", + "44444444", + "55555555", + "66666666", + "77777777", + "88888888", + "99999999" + ], + ]; + } + + if ($type === 'manager') { + return [ + "id" => 5678, + "data" => [], + ]; + } + + throw new InvalidArgumentException(sprintf( + 'This Fake ID Broker class does not support creating %s MFA records.', + $type + )); + } + + /** + * Get a list of MFA configurations for given user + * @param string $employee_id + * @return array + * @throws ServiceException + */ + public function mfaList($employee_id) + { + return [ + [ + 'id' => 1, + 'type' => 'backupcode', + 'label' => 'Printable Codes', + 'created_utc' => '2019-04-02T16:02:14Z', + 'last_used_utc' => '2019-04-01T00:00:00Z', + 'data' => [ + 'count' => 10 + ], + ], + [ + 'id' => 2, + 'type' => 'totp', + 'label' => 'Smartphone App', + 'created_utc' => '2019-04-02T16:02:14Z', + 'last_used_utc' => '2019-04-01T00:00:00Z', + 'data' => [ + ], + ], + ]; + } +} diff --git a/features/mfa.feature b/features/mfa.feature index 2700429b..a3a00432 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -1,21 +1,241 @@ -Feature: Multi-Factor Authentication (MFA) module - - Scenario: Low on backup codes - - Scenario: Must set up MFA - - Scenario: New backup codes - - Scenario: Other MFAs - - Scenario: Out of backup codes - - Scenario: Prompt for MFA (backup code) - - Scenario: Prompt for MFA (manager) - - Scenario: Prompt for MFA (TOTP) - - Scenario: Prompt for MFA (U2F) - - Scenario: Send manager MFA +Feature: Prompt for MFA credentials + + Scenario: Don't prompt for MFA + Given I provide credentials that do not need MFA + When I login + Then I should end up at my intended destination + + Scenario: Needs MFA, but no MFA options are available + Given I provide credentials that need MFA but have no MFA options available + When I login + Then I should see a message that I have to set up MFA + And there should be a way to go set up MFA now + And there should NOT be a way to continue to my intended destination + + Scenario: Following the requirement to go set up MFA + Given I provide credentials that need MFA but have no MFA options available + And I login + When I click the set-up-MFA button + Then I should end up at the mfa-setup URL + And I should NOT be able to get to my intended destination + + Scenario: Needs MFA, has backup code option available + Given I provide credentials that need MFA and have backup codes available + When I login + Then I should see a prompt for a backup code + + Scenario: Needs MFA, has TOTP option available + Given I provide credentials that need MFA and have TOTP available + When I login + Then I should see a prompt for a TOTP code + + Scenario: Needs MFA, has WebAuthn option available + Given I provide credentials that need MFA and have WebAuthn available + And the user's browser supports WebAuthn + When I login + Then I should see a prompt for a WebAuthn security key + + Scenario: Accepting a (non-rate-limited) correct MFA value + Given I provide credentials that need MFA and have backup codes available + And I have logged in + When I submit a correct backup code + Then I should end up at my intended destination + + Scenario: Rejecting a (non-rate-limited) wrong MFA value + Given I provide credentials that need MFA and have backup codes available + And I have logged in + When I submit an incorrect backup code + Then I should see a message that it was incorrect + + Scenario: Blocking an incorrect MFA value while rate-limited + Given I provide credentials that have a rate-limited MFA + And I have logged in + When I submit an incorrect backup code + Then I should see a message that I have to wait before trying again + + Scenario: Blocking a correct MFA value while rate-limited + Given I provide credentials that have a rate-limited MFA + And I have logged in + When I submit a correct backup code + Then I should see a message that I have to wait before trying again + + Scenario: Warning when running low on backup codes + Given I provide credentials that need MFA and have 4 backup codes available + And I have logged in + When I submit a correct backup code + Then I should see a message that I am running low on backup codes + And I should be told I only have 3 backup codes left + And there should be a way to get more backup codes now + And there should be a way to continue to my intended destination + + Scenario: Requiring user to set up more backup codes when they run out and have no other MFA + Given I provide credentials that need MFA and have 1 backup code available and no other MFA + And I have logged in + When I submit a correct backup code + Then I should see a message that I have used up my backup codes + And there should be a way to get more backup codes now + And there should NOT be a way to continue to my intended destination + + Scenario: Warning user when they run out of backup codes but have other MFA options + Given I provide credentials that need MFA and have 1 backup code available plus some other MFA + And I have logged in + When I submit a correct backup code + Then I should see a message that I have used up my backup codes + And there should be a way to get more backup codes now + And there should be a way to continue to my intended destination + + Scenario: Obeying the nag to set up more backup codes when low + Given I provide credentials that need MFA and have 4 backup codes available + And I have logged in + And I submit a correct backup code + When I click the get-more-backup-codes button + Then I should be given more backup codes + And there should be a way to continue to my intended destination + + Scenario: Ignoring the nag to set up more backup codes when low + Given I provide credentials that need MFA and have 4 backup codes available + And I have logged in + And I submit a correct backup code + When I click the remind-me-later button + Then I should end up at my intended destination + + Scenario: Obeying the requirement to set up more backup codes when out + Given I provide credentials that need MFA and have 1 backup code available and no other MFA + And I have logged in + And I submit a correct backup code + When I click the get-more-backup-codes button + Then I should be given more backup codes + And there should be a way to continue to my intended destination + + Scenario: Obeying the nag to set up more backup codes when out + Given I provide credentials that need MFA and have 1 backup code available plus some other MFA + And I have logged in + And I submit a correct backup code + When I click the get-more-backup-codes button + Then I should be given more backup codes + And there should be a way to continue to my intended destination + + Scenario: Ignoring the nag to set up more backup codes when out + Given I provide credentials that need MFA and have 1 backup code available plus some other MFA + And I have logged in + And I submit a correct backup code + When I click the remind-me-later button + Then I should end up at my intended destination + + Scenario Outline: Defaulting to another option when WebAuthn is not supported + Given I provide credentials that have + And the user's browser + When I login + Then I should see a prompt for a + + Examples: + | WebAuthn? | TOTP? | backup codes? | supports WebAuthn or not | default MFA type | + | WebAuthn | | | supports WebAuthn | WebAuthn | + | WebAuthn | , TOTP | | supports WebAuthn | WebAuthn | + | WebAuthn | | , backup codes | supports WebAuthn | WebAuthn | + | WebAuthn | , TOTP | , backup codes | supports WebAuthn | WebAuthn | + | | TOTP | | supports WebAuthn | TOTP | + | | TOTP | , backup codes | supports WebAuthn | TOTP | + | | | backup codes | supports WebAuthn | backup code | + | WebAuthn | | | does not support WebAuthn | WebAuthn | + | WebAuthn | , TOTP | | does not support WebAuthn | TOTP | + | WebAuthn | | , backup codes | does not support WebAuthn | backup code | + | WebAuthn | , TOTP | , backup codes | does not support WebAuthn | TOTP | + | | TOTP | | does not support WebAuthn | TOTP | + | | TOTP | , backup codes | does not support WebAuthn | TOTP | + | | | backup codes | does not support WebAuthn | backup code | + + + Scenario Outline: Defaulting to the most recently used mfa option + Given I provide credentials that have a used + And and I have a more recently used + And the user's browser + When I login + Then I should see a prompt for a + + Examples: + | MFA type | recent MFA type | supports WebAuthn or not | default MFA type | + | WebAuthn | TOTP | supports WebAuthn | TOTP | + | TOTP | WebAuthn | supports WebAuthn | WebAuthn | + | TOTP | backup code | supports WebAuthn | backup code | + | backup code | TOTP | supports WebAuthn | TOTP | + | TOTP | WebAuthn | does not support WebAuthn | TOTP | + + Scenario: Defaulting to the manager code despite having a used mfa + Given I provide credentials that have a manager code, a WebAuthn and a more recently used TOTP + And the user's browser supports WebAuthn + When I login + Then I should see a prompt for a manager rescue code + + Scenario Outline: When to show the WebAuthn-not-supported error message + Given I provide credentials that have WebAuthn + And the user's browser + When I login + Then I see an error message about WebAuthn being unsupported + + Examples: + | supports WebAuthn or not | should or not | + | supports WebAuthn | should not | + | does not support WebAuthn | should | + + Scenario Outline: When to show the link to send a manager rescue code + Given I provide credentials that have + And the user a manager email + When I login + Then I see a link to send a code to the user's manager + + Examples: + | WebAuthn? | TOTP? | backup codes? | has or does not have | should or should not | + | WebAuthn | | | has | should | + | WebAuthn | , TOTP | | has | should | + | WebAuthn | | , backup codes | has | should | + | WebAuthn | , TOTP | , backup codes | has | should | + | | TOTP | | has | should | + | | TOTP | , backup codes | has | should | + | | | backup codes | has | should | + | WebAuthn | | | does not have | should not | + | WebAuthn | , TOTP | | does not have | should not | + | WebAuthn | | , backup codes | does not have | should not | + | WebAuthn | , TOTP | , backup codes | does not have | should not | + | | TOTP | | does not have | should not | + | | TOTP | , backup codes | does not have | should not | + | | | backup codes | does not have | should not | + + Scenario: Ask for a code to be sent to my manager + Given I provide credentials that have backup codes + And the user has a manager email + And I login + When I click the Request Assistance link + Then there should be a way to request a manager code + + Scenario: Submit a code sent to my manager at an earlier time + Given I provide credentials that have a manager code + And I login + When I submit the correct manager code + Then I should end up at my intended destination + + Scenario: Submit a correct manager code + Given I provide credentials that have backup codes + And the user has a manager email + And I login + And I click the Request Assistance link + And I click the Send a code link + When I submit the correct manager code + Then I should end up at my intended destination + + Scenario: Submit an incorrect manager code + Given I provide credentials that have backup codes + And the user has a manager email + And I login + And I click the Request Assistance link + And I click the Send a code link + When I submit an incorrect manager code + Then I should see a message that it was incorrect + + Scenario: Ask for assistance, but change my mind + Given I provide credentials that have backup codes + And the user has a manager email + And I login + And I click the Request Assistance link + When I click the Cancel button + Then I should see a prompt for a backup code diff --git a/local.env.dist b/local.env.dist index ac3f00f7..23c9cdbf 100644 --- a/local.env.dist +++ b/local.env.dist @@ -74,3 +74,9 @@ PROFILE_URL= # An absolute URL the user can go to to learn more about MFA. MFA_LEARN_MORE_URL= + +# MFA config + +# The URL to send a user to for setting up their MFA. +# Example: https://pw.example.com/#/2sv/intro +MFA_SETUP_URL= diff --git a/modules/mfa/lib/Auth/Process/Mfa.php b/modules/mfa/lib/Auth/Process/Mfa.php new file mode 100644 index 00000000..2eec44b5 --- /dev/null +++ b/modules/mfa/lib/Auth/Process/Mfa.php @@ -0,0 +1,956 @@ +initComposerAutoloader(); + assert('is_array($config)'); + + $this->loggerClass = $config['loggerClass'] ?? Psr3SamlLogger::class; + $this->logger = LoggerFactory::get($this->loggerClass); + + $this->loadValuesFromConfig($config, [ + 'mfaSetupUrl', + 'employeeIdAttr', + 'idBrokerAccessToken', + 'idBrokerBaseUri', + 'idpDomainName', + ]); + + $tempTrustedIpRanges = $config['idBrokerTrustedIpRanges'] ?? ''; + if (! empty($tempTrustedIpRanges)) { + $this->idBrokerTrustedIpRanges = explode(',', $tempTrustedIpRanges); + } + $this->idBrokerAssertValidIp = (bool)($config['idBrokerAssertValidIp'] ?? true); + $this->idBrokerClientClass = $config['idBrokerClientClass'] ?? IdBrokerClient::class; + } + + 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; + } + + /** + * Get an ID Broker client. + * + * @param array $idBrokerConfig + * @return IdBrokerClient + */ + protected static function getIdBrokerClient($idBrokerConfig) + { + $clientClass = $idBrokerConfig['clientClass']; + $baseUri = $idBrokerConfig['baseUri']; + $accessToken = $idBrokerConfig['accessToken']; + $trustedIpRanges = $idBrokerConfig['trustedIpRanges']; + $assertValidIp = $idBrokerConfig['assertValidIp']; + + return new $clientClass($baseUri, $accessToken, [ + 'http_client_options' => [ + 'timeout' => 10, + ], + IdBrokerClient::TRUSTED_IPS_CONFIG => $trustedIpRanges, + IdBrokerClient::ASSERT_VALID_BROKER_IP_CONFIG => $assertValidIp, + ]); + } + + /** + * Get the MFA type to use based on the available options. + * + * @param array[] $mfaOptions The available MFA options. + * @param int $mfaId The ID of the desired MFA option. + * @return array The MFA option to use. + * @throws \InvalidArgumentException + * @throws \Exception + */ + public static function getMfaOptionById($mfaOptions, $mfaId) + { + if (empty($mfaId)) { + throw new \Exception('No MFA ID was provided.'); + } + + foreach ($mfaOptions as $mfaOption) { + if ((int)$mfaOption['id'] === (int)$mfaId) { + return $mfaOption; + } + } + + throw new \Exception( + 'No MFA option has an ID of ' . var_export($mfaId, true) + ); + } + + /** + * Get the MFA type to use based on the available options. + * + * @param array[] $mfaOptions The available MFA options. + * @param string $userAgent The User-Agent sent by the user's browser, used + * for detecting WebAuthn support. + * @return array The MFA option to use. + * @throws \InvalidArgumentException + * @throws \Exception + */ + public static function getMfaOptionToUse($mfaOptions, $userAgent) + { + if (empty($mfaOptions)) { + throw new \Exception('No MFA options were provided.'); + } + + $recentMfa = self::getMostRecentUsedMfaOption($mfaOptions); + $mfaTypePriority = ['manager']; + + if (LoginBrowser::supportsWebAuthn($userAgent)) { + if (isset($recentMfa['type'])) { + $mfaTypePriority[] = $recentMfa['type']; + } + // Doubling up a type shouldn't be a problem. + array_push($mfaTypePriority, 'webauthn', 'totp', 'backupcode'); + } else { + // Browser doesn't support webauthn, so ensure that's the last option + if (isset($recentMfa['type']) && $recentMfa['type'] != 'webauthn') { + $mfaTypePriority[] = $recentMfa['type']; + } + array_push($mfaTypePriority, 'totp', 'backupcode', 'webauthn'); + } + + foreach ($mfaTypePriority as $mfaType) { + foreach ($mfaOptions as $mfaOption) { + if ($mfaOption['type'] === $mfaType) { + return $mfaOption; + } + } + } + + return $mfaOptions[0]; + } + + /** + * Get the MFA to use based on the one used most recently. + * + * @param array[] $mfaOptions The available MFA options. + * @return ?array The MFA option to use. + */ + private static function getMostRecentUsedMfaOption($mfaOptions) { + $recentMfa = null; + $recentDate = '1991-01-01T00:00:00Z'; + + foreach ($mfaOptions as $mfaOption) { + if (isset($mfaOption['last_used_utc']) && $mfaOption['last_used_utc'] > $recentDate) { + $recentMfa = $mfaOption; + $recentDate = $mfaOption['last_used_utc']; + } + } + return $recentMfa; + } + + /** + * Get the number of backup codes that the user had left PRIOR to this login. + * + * @param array $mfaOptions The list of MFA options. + * @return int The number of backup codes that the user HAD (prior to this + * login). + */ + public static function getNumBackupCodesUserHad(array $mfaOptions) + { + $numBackupCodes = 0; + foreach ($mfaOptions as $mfaOption) { + $mfaType = $mfaOption['type'] ?? null; + if ($mfaType === 'backupcode') { + $numBackupCodes += intval($mfaOption['data']['count'] ?? 0); + } + } + + return $numBackupCodes; + } + + /** + * Get the template identifier (string) to use for the specified MFA type. + * + * @param string $mfaType The desired MFA type, such as 'webauthn', 'totp', or 'backupcode'. + * @return string + * @throws \InvalidArgumentException + */ + public static function getTemplateFor($mfaType) + { + $mfaOptionTemplates = [ + 'backupcode' => 'mfa:prompt-for-mfa-backupcode.php', + 'totp' => 'mfa:prompt-for-mfa-totp.php', + 'webauthn' => 'mfa:prompt-for-mfa-webauthn.php', + 'manager' => 'mfa:prompt-for-mfa-manager.php', + ]; + $template = $mfaOptionTemplates[$mfaType] ?? null; + + if ($template === null) { + throw new \InvalidArgumentException(sprintf( + 'No %s MFA template is available.', + var_export($mfaType, true) + ), 1507219338); + } + return $template; + } + + /** + * Return the saml:RelayState if it begins with "http" or "https". Otherwise + * return an empty string. + * + * @param array $state + * @return 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 ''; + } + + /** + * Get new Printable Backup Codes for the user, then redirect the user to a + * page showing the user their new codes. + * + * NOTE: This function never returns. + * + * @param array $state The state data. + * @param LoggerInterface $logger A PSR-3 compatible logger. + */ + public static function giveUserNewBackupCodes(array &$state, $logger) + { + try { + $idBrokerClient = self::getIdBrokerClient($state['idBrokerConfig']); + $newMfaRecord = $idBrokerClient->mfaCreate( + $state['employeeId'], + 'backupcode' + ); + $newBackupCodes = $newMfaRecord['data']; + + $logger->warning(json_encode([ + 'event' => 'New backup codes result: succeeded', + 'employeeId' => $state['employeeId'], + ])); + } catch (\Throwable $t) { + $logger->error(json_encode([ + 'event' => 'New backup codes result: failed', + 'employeeId' => $state['employeeId'], + 'error' => $t->getCode() . ': ' . $t->getMessage(), + ])); + } + + self::updateStateWithNewMfaData($state, $logger); + + $state['newBackupCodes'] = $newBackupCodes ?? null; + $stateId = State::saveState($state, self::STAGE_SENT_TO_NEW_BACKUP_CODES_PAGE); + $url = Module::getModuleURL('mfa/new-backup-codes.php'); + + HTTP::redirectTrustedURL($url, ['StateId' => $stateId]); + } + + protected static function hasMfaOptions($mfa) + { + return (count($mfa['options']) > 0); + } + + /** + * See if the user has any MFA options other than the specified type. + * + * @param string $excludedMfaType + * @param array $state + * @return bool + */ + public static function hasMfaOptionsOtherThan($excludedMfaType, $state) + { + $mfaOptions = $state['mfaOptions'] ?? []; + foreach ($mfaOptions as $mfaOption) { + if (strval($mfaOption['type']) !== strval($excludedMfaType)) { + return true; + } + } + return false; + } + + protected function initComposerAutoloader() + { + $path = __DIR__ . '/../../../vendor/autoload.php'; + if (file_exists($path)) { + require_once $path; + } + } + + protected static function isHeadedToMfaSetupUrl($state, $mfaSetupUrl) + { + if (array_key_exists('saml:RelayState', $state)) { + $currentDestination = self::getRelayStateUrl($state); + if (! empty($currentDestination)) { + return (strpos($currentDestination, $mfaSetupUrl) === 0); + } + } + return false; + } + + /** + * Validate the given MFA submission. If successful, this function + * will NOT return. If the submission does not pass validation, an error + * message will be returned. + * + * @param int $mfaId The ID of the MFA option used. + * @param string $employeeId The Employee ID that this MFA option belongs to. + * @param string $mfaSubmission The value of the MFA submission. + * @param array $state The array of state information. + * @param bool $rememberMe Whether or not to set remember me cookies + * @param LoggerInterface $logger A PSR-3 compatible logger. + * @param string $mfaType The type of the MFA ('webauthn', 'totp', 'backupcode'). + * @param string $rpOrigin The Relying Party Origin (for WebAuthn) + * @return void|string If validation fails, an error message to show to the + * end user will be returned. + * @throws \Sil\PhpEnv\EnvVarNotFoundException + */ + public static function validateMfaSubmission( + $mfaId, + $employeeId, + $mfaSubmission, + $state, + $rememberMe, + LoggerInterface $logger, + string $mfaType, + string $rpOrigin + ) { + if (empty($mfaId)) { + return 'No MFA ID was provided.'; + } elseif (empty($employeeId)) { + return 'No Employee ID was provided.'; + } elseif (empty($mfaSubmission)) { + return 'No MFA submission was provided.'; + } elseif (empty($rpOrigin)) { + return 'No RP Origin was provided.'; + } + + try { + $idBrokerClient = self::getIdBrokerClient($state['idBrokerConfig']); + $mfaDataFromBroker = $idBrokerClient->mfaVerify( + $mfaId, + $employeeId, + $mfaSubmission, + $rpOrigin + ); + } catch (\Throwable $t) { + $message = 'Something went wrong while we were trying to do the ' + . '2-step verification.'; + if ($t instanceof ServiceException) { + if ($t->httpStatusCode === 400) { + if ($mfaType === 'backupcode') { + return 'Incorrect 2-step verification code. Printable backup ' + . 'codes can only be used once, please try a different code.'; + } + return 'Incorrect 2-step verification code.'; + } elseif ($t->httpStatusCode === 429){ + $logger->error(json_encode([ + 'event' => 'MFA is rate-limited', + 'employeeId' => $employeeId, + 'mfaId' => $mfaId, + 'mfaType' => $mfaType, + ])); + return 'There have been too many wrong answers recently. ' + . 'Please wait a minute, then try again.'; + } else { + $message .= ' (code ' . $t->httpStatusCode . ')'; + return $message; + } + } + + $logger->critical($t->getCode() . ': ' . $t->getMessage()); + return $message; + } + + self::updateStateWithNewMfaData($state, $logger); + + // Set remember me cookies if requested + if ($rememberMe) { + self::setRememberMeCookies($state['employeeId'], $state['mfaOptions']); + } + + $logger->warning(json_encode([ + 'event' => 'MFA validation result: success', + 'employeeId' => $employeeId, + 'mfaType' => $mfaType, + ])); + + // Handle situations where the user is running low on backup codes. + if ($mfaType === 'backupcode') { + $numBackupCodesUserHad = self::getNumBackupCodesUserHad( + $state['mfaOptions'] ?? [] + ); + $numBackupCodesRemaining = $numBackupCodesUserHad - 1; + + if ($numBackupCodesRemaining <= 0) { + self::redirectToOutOfBackupCodesMessage($state, $employeeId); + throw new \Exception('Failed to send user to out-of-backup-codes page.'); + } elseif ($numBackupCodesRemaining < 4) { + self::redirectToLowOnBackupCodesNag( + $state, + $employeeId, + $numBackupCodesRemaining + ); + throw new \Exception('Failed to send user to low-on-backup-codes page.'); + } + } + + /* + * If the user had to use a manager code, show the profile review page. + */ + if ($mfaType === 'manager' && isset($state['Attributes']['profile_review'])) { + $state['Attributes']['profile_review'] = 'yes'; + } + + unset($state['Attributes']['manager_email']); + + // The following function call will never return. + ProcessingChain::resumeProcessing($state); + throw new \Exception('Failed to resume processing auth proc chain.'); + } + + /** + * Redirect the user to set up MFA. + * + * @param array $state + */ + public static function redirectToMfaSetup(&$state) + { + $mfaSetupUrl = $state['mfaSetupUrl']; + + // Tell the MFA-setup URL where the user is ultimately trying to go (if known). + $currentDestination = self::getRelayStateUrl($state); + if (! empty($currentDestination)) { + $mfaSetupUrl = HTTP::addURLParameters( + $mfaSetupUrl, + ['returnTo' => $currentDestination] + ); + } + + $logger = LoggerFactory::getAccordingToState($state); + $logger->warning(sprintf( + 'mfa: Sending Employee ID %s to set up MFA at %s', + var_export($state['employeeId'] ?? null, true), + var_export($mfaSetupUrl, true) + )); + + HTTP::redirectTrustedURL($mfaSetupUrl); + } + + /** + * 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); + $mfa = $this->getAttributeAllValues('mfa', $state); + $isHeadedToMfaSetupUrl = self::isHeadedToMfaSetupUrl( + $state, + $this->mfaSetupUrl + ); + + // Record to the state what logger class to use. + $state['loggerClass'] = $this->loggerClass; + + // Add to the state any config data we may need for the low-on/out-of + // backup codes pages. + $state['mfaSetupUrl'] = $this->mfaSetupUrl; + + if (self::shouldPromptForMfa($mfa)) { + if (self::hasMfaOptions($mfa)) { + $this->redirectToMfaPrompt($state, $employeeId, $mfa['options']); + return; + } + + if (! $isHeadedToMfaSetupUrl) { + $this->redirectToMfaNeededMessage($state, $employeeId, $this->mfaSetupUrl); + return; + } + } + + unset($state['Attributes']['manager_email']); + } + + /** + * Redirect the user to a page telling them they must set up MFA. + * + * @param array $state The state data. + * @param string $employeeId The Employee ID of the user account. + * @param string $mfaSetupUrl URL to MFA setup process + */ + protected function redirectToMfaNeededMessage(&$state, $employeeId, $mfaSetupUrl) + { + assert('is_array($state)'); + + $this->logger->info(sprintf( + 'mfa: Redirecting Employee ID %s to must-set-up-MFA message.', + var_export($employeeId, true) + )); + + /* Save state and redirect. */ + $state['employeeId'] = $employeeId; + $state['mfaSetupUrl'] = $mfaSetupUrl; + + $stateId = State::saveState($state, self::STAGE_SENT_TO_MFA_NEEDED_MESSAGE); + $url = Module::getModuleURL('mfa/must-set-up-mfa.php'); + + HTTP::redirectTrustedURL($url, ['StateId' => $stateId]); + } + + /** + * Redirect the user to the appropriate MFA-prompt page. + * + * @param array $state The state data. + * @param string $employeeId The Employee ID of the user account. + * @param array $mfaOptions Array of MFA options + * @throws \Exception + */ + protected function redirectToMfaPrompt(&$state, $employeeId, $mfaOptions) + { + assert('is_array($state)'); + + /** @todo Check for valid remember-me cookies here rather doing a redirect first. */ + + $state['mfaOptions'] = $mfaOptions; + $state['managerEmail'] = self::getManagerEmail($state); + $state['idBrokerConfig'] = [ + 'accessToken' => $this->idBrokerAccessToken, + 'assertValidIp' => $this->idBrokerAssertValidIp, + 'baseUri' => $this->idBrokerBaseUri, + 'clientClass' => $this->idBrokerClientClass, + 'trustedIpRanges' => $this->idBrokerTrustedIpRanges, + ]; + + $this->logger->info(sprintf( + 'mfa: Redirecting Employee ID %s to MFA prompt.', + var_export($employeeId, true) + )); + + /* Save state and redirect. */ + $state['employeeId'] = $employeeId; + $state['rpOrigin'] = 'https://' . $this->idpDomainName; + + $id = State::saveState($state, self::STAGE_SENT_TO_MFA_PROMPT); + $url = Module::getModuleURL('mfa/prompt-for-mfa.php'); + + $mfaOption = self::getMfaOptionToUse($mfaOptions, LoginBrowser::getUserAgent()); + + HTTP::redirectTrustedURL($url, [ + 'mfaId' => $mfaOption['id'], + 'StateId' => $id, + ]); + } + + /** + * Validate that remember me cookie values are legit and valid + * @param string $cookieHash + * @param string $expireDate + * @param $mfaOptions + * @param $state + * @return bool + * @throws \Sil\PhpEnv\EnvVarNotFoundException + */ + public static function isRememberMeCookieValid( + string $cookieHash, + string $expireDate, + $mfaOptions, + $state + ): bool { + $rememberSecret = Env::requireEnv('REMEMBER_ME_SECRET'); + if (! empty($cookieHash) && ! empty($expireDate) && is_numeric($expireDate)) { + // Check if value of expireDate is in future + if ((int)$expireDate > time()) { + $expectedString = self::generateRememberMeCookieString($rememberSecret, $state['employeeId'], $expireDate, $mfaOptions); + return password_verify($expectedString, $cookieHash); + } + } + + return false; + } + + /** + * Generate and return a string to be hashed for remember me cookie + * @param string $rememberSecret + * @param string $employeeId + * @param int $expireDate + * @param array $mfaOptions + * @return string + */ + public static function generateRememberMeCookieString( + string $rememberSecret, + string $employeeId, + int $expireDate, + array $mfaOptions + ): string { + $allMfaIds = ''; + foreach ($mfaOptions as $opt) { + if ($opt['type'] !== 'manager') { + $allMfaIds .= $opt['id']; + } + } + + $string = $rememberSecret . $employeeId . $expireDate . $allMfaIds; + return $string; + } + + /** + * Redirect the user to a page telling them they are running low on backup + * codes and encouraging them to create more now. + * + * NOTE: This function never returns. + * + * @param array $state The state data. + * @param string $employeeId The Employee ID of the user account. + * @param int $numBackupCodesRemaining The number of backup codes that the + * user has left (now that they have used up one for this login). + */ + protected static function redirectToLowOnBackupCodesNag( + array &$state, + $employeeId, + $numBackupCodesRemaining + ) { + $state['employeeId'] = $employeeId; + $state['numBackupCodesRemaining'] = $numBackupCodesRemaining; + + $stateId = State::saveState($state, self::STAGE_SENT_TO_LOW_ON_BACKUP_CODES_NAG); + $url = Module::getModuleURL('mfa/low-on-backup-codes.php'); + + HTTP::redirectTrustedURL($url, ['StateId' => $stateId]); + } + + /** + * Redirect the user to a page telling them they just used up their last + * backup code. + * + * NOTE: This function never returns. + * + * @param array $state The state data. + * @param string $employeeId The Employee ID of the user account. + */ + protected static function redirectToOutOfBackupCodesMessage(array &$state, $employeeId) + { + $state['employeeId'] = $employeeId; + + $stateId = State::saveState($state, self::STAGE_SENT_TO_OUT_OF_BACKUP_CODES_MESSAGE); + $url = Module::getModuleURL('mfa/out-of-backup-codes.php'); + + HTTP::redirectTrustedURL($url, ['StateId' => $stateId]); + } + + /** + * Set cookies c1 and c2 + * @param string $employeeId + * @param array $mfaOptions + * @param string $rememberDuration + * @throws \Sil\PhpEnv\EnvVarNotFoundException + */ + public static function setRememberMeCookies( + string $employeeId, + array $mfaOptions, + string $rememberDuration = '+30 days' + ) { + $rememberSecret = Env::requireEnv('REMEMBER_ME_SECRET'); + $secureCookie = Env::get('SECURE_COOKIE', true); + $expireDate = strtotime($rememberDuration); + $cookieString = self::generateRememberMeCookieString($rememberSecret, $employeeId, $expireDate, $mfaOptions); + $cookieHash = password_hash($cookieString, PASSWORD_DEFAULT); + setcookie('c1', base64_encode($cookieHash), $expireDate, '/', null, $secureCookie, true); + setcookie('c2', $expireDate, $expireDate, '/', null, $secureCookie, true); + } + + protected static function shouldPromptForMfa($mfa) + { + return (strtolower($mfa['prompt']) !== 'no'); + } + + /** + * Send a rescue code to the manager, then redirect the user to a page where they + * can enter the code. + * + * NOTE: This function never returns. + * + * @param array $state The state data. + * @param LoggerInterface $logger A PSR-3 compatible logger. + */ + public static function sendManagerCode(array &$state, $logger) + { + try { + $idBrokerClient = self::getIdBrokerClient($state['idBrokerConfig']); + $mfaOption = $idBrokerClient->mfaCreate($state['employeeId'], 'manager'); + $mfaOption['type'] = 'manager'; + + $logger->warning(json_encode([ + 'event' => 'Manager rescue code sent', + 'employeeId' => $state['employeeId'], + ])); + } catch (\Throwable $t) { + $logger->error(json_encode([ + 'event' => 'Manager rescue code: failed', + 'employeeId' => $state['employeeId'], + 'error' => $t->getCode() . ': ' . $t->getMessage(), + ])); + } + + $mfaOptions = $state['mfaOptions']; + + /* + * Add this option into the list, giving it a key so `mfaOptions` doesn't get multiple entries + * if the user tries multiple times. + */ + $mfaOptions['manager'] = $mfaOption; + $state['mfaOptions'] = $mfaOptions; + $state['managerEmail'] = self::getManagerEmail($state); + $stateId = State::saveState($state, self::STAGE_SENT_TO_MFA_PROMPT); + + $url = Module::getModuleURL('mfa/prompt-for-mfa.php'); + + HTTP::redirectTrustedURL($url, ['mfaId' => $mfaOption['id'], 'StateId' => $stateId]); + } + + /** + * Get masked copy of manager_email, or null if it isn't available. + * + * @param array $state + * @return string|null + */ + public static function getManagerEmail($state) + { + $managerEmail = $state['Attributes']['manager_email'] ?? ['']; + if (empty($managerEmail[0])) { + return null; + } + return self::maskEmail($managerEmail[0]); + } + + /** + * Get the manager MFA, if it exists. Otherwise, return null. + * + * @param array[] $mfaOptions The available MFA options. + * @return array The manager MFA. + * @throws \InvalidArgumentException + */ + public static function getManagerMfa($mfaOptions) + { + foreach ($mfaOptions as $mfaOption) { + if ($mfaOption['type'] === 'manager') { + return $mfaOption; + } + } + + return null; + } + + /** + * @param string $email an email address + * @return string with most letters changed to asterisks + */ + public static function maskEmail($email) + { + list($part1, $domain) = explode('@', $email); + $newEmail = ''; + $useRealChar = true; + + /* + * Replace all characters with '*', except + * the first one, the last one, underscores and each + * character that follows and underscore. + */ + foreach (str_split($part1) as $nextChar) { + if ($useRealChar) { + $newEmail .= $nextChar; + $useRealChar = false; + } else if ($nextChar === '_') { + $newEmail .= $nextChar; + $useRealChar = true; + } else { + $newEmail .= '*'; + } + } + + // replace the last * with the last real character + $newEmail = substr($newEmail, 0, -1); + $newEmail .= substr($part1, -1); + $newEmail .= '@'; + + /* + * Add an '*' for each of the characters of the domain, except + * for the first character of each part and the . + */ + list($domainA, $domainB) = explode('.', $domain); + + $newEmail .= substr($domainA, 0, 1); + $newEmail .= str_repeat('*', strlen($domainA) - 1); + $newEmail .= '.'; + + $newEmail .= substr($domainB, 0, 1); + $newEmail .= str_repeat('*', strlen($domainB) - 1); + return $newEmail; + } + + /** + * @param array $state + * @param LoggerInterface $logger + */ + protected static function updateStateWithNewMfaData(&$state, $logger) + { + $idBrokerClient = self::getIdBrokerClient($state['idBrokerConfig']); + + $log = [ + 'event' => 'Update state with new mfa data', + ]; + + try { + $newMfaOptions = $idBrokerClient->mfaList($state['employeeId']); + } catch (\Exception $e) { + $log['status'] = 'failed: id-broker exception'; + $logger->error(json_encode($log)); + return; + } + + if (empty($newMfaOptions)) { + $log['status'] = 'failed: no data provided'; + $logger->warning(json_encode($log)); + return; + } + + $state['Attributes']['mfa']['options'] = $newMfaOptions; + + $log['data'] = $newMfaOptions; + $log['status'] = 'updated'; + $logger->warning(json_encode($log)); + } +} diff --git a/modules/mfa/src/Assert.php b/modules/mfa/src/Assert.php new file mode 100644 index 00000000..eadbbb20 --- /dev/null +++ b/modules/mfa/src/Assert.php @@ -0,0 +1,57 @@ +getName(); + + // For now, simply set these to approximate the results shown on caniuse: + // https://caniuse.com/?search=webauthn + return in_array( + $browserName, + [ + Browser::CHROME, + Browser::SAFARI, + Browser::EDGE, + Browser::FIREFOX, + Browser::OPERA, + ], + true + ); + } +} diff --git a/modules/mfa/templates/low-on-backup-codes.php b/modules/mfa/templates/low-on-backup-codes.php new file mode 100644 index 00000000..bf1f7a37 --- /dev/null +++ b/modules/mfa/templates/low-on-backup-codes.php @@ -0,0 +1,21 @@ +data['header'] = 'Almost out of Printable Backup Codes'; +$this->includeAtTemplateBase('includes/header.php'); + +$numBackupCodesRemaining = $this->data['numBackupCodesRemaining']; +?> +

+ You are almost out of Printable Backup Codes. + You only have remaining. +

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

+ Your account requires additional security. + You must set up 2-step verification at this time. +

+
+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/templates/new-backup-codes.php b/modules/mfa/templates/new-backup-codes.php new file mode 100644 index 00000000..5624a681 --- /dev/null +++ b/modules/mfa/templates/new-backup-codes.php @@ -0,0 +1,40 @@ +data['header'] = 'New Printable Backup Codes'; +$this->includeAtTemplateBase('includes/header.php'); + +$newBackupCodes = $this->data['newBackupCodes']; +$mfaSetupUrl = $this->data['mfaSetupUrl']; +?> + + +

+ Something went wrong while we were trying to get more Printable Backup Codes for you. +

+

+ We are sorry for the inconvenience. After you finish logging in, please + check your 2-Step Verification methods here:
+ +

+ +

+ Here are your new Printable Backup Codes. Remember to keep them + secret (like a password) and store them somewhere safe. +

+

+

+ Once you have stored them somewhere safe, you are welcome to click the + button below to continue to where you were going. +

+ + +
+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/templates/out-of-backup-codes.php b/modules/mfa/templates/out-of-backup-codes.php new file mode 100644 index 00000000..27005578 --- /dev/null +++ b/modules/mfa/templates/out-of-backup-codes.php @@ -0,0 +1,37 @@ +data['header'] = 'Last Printable Backup Code'; +$this->includeAtTemplateBase('includes/header.php'); + +$hasOtherMfaOptions = $this->data['hasOtherMfaOptions']; +?> +

+ You just used your last Printable Backup Code. +

+ + +

+ We recommend you get more now so that you will have some next time we ask + you for one. Otherwise, you will need to use a different option (such as a + Security Key or Smartphone App) the next time we ask you for 2-Step Verification. +

+ +

+ Since you do not have any other 2-Step Verification options set up yet, + you need to get more Printable Backup Codes now so that you will have some + next time we ask you for one. +

+ + +
+ + + + + +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/templates/prompt-for-mfa-backupcode.php b/modules/mfa/templates/prompt-for-mfa-backupcode.php new file mode 100644 index 00000000..3fb79ef5 --- /dev/null +++ b/modules/mfa/templates/prompt-for-mfa-backupcode.php @@ -0,0 +1,57 @@ +data['header'] = '2-Step Verification'; +$this->includeAtTemplateBase('includes/header.php'); + +if (! empty($this->data['errorMessage'])) { + ?> +

+ Oops!
+ data['errorMessage']); ?> +

+ +
+

Printable Backup Code

+

+ Each code can only be used once, so the code you enter this time will be + used up and will not be available again. +

+

+ Enter code: +
+ + +
+ +

+ data['mfaOptions']) > 1): ?> +

+ Don't have your printable backup codes handy? You may also use: +

+
    + data['mfaOptions'] as $mfaOpt) { + if ($mfaOpt['type'] != 'backupcode') { + ?> +
  • + +
+ + data['managerEmail'])): ?> +

+ Can't use any of your 2-Step Verification options? + + Click here for assistance. +

+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/templates/prompt-for-mfa-manager.php b/modules/mfa/templates/prompt-for-mfa-manager.php new file mode 100644 index 00000000..643f9631 --- /dev/null +++ b/modules/mfa/templates/prompt-for-mfa-manager.php @@ -0,0 +1,49 @@ +data['header'] = '2-Step Verification'; +$this->includeAtTemplateBase('includes/header.php'); + +if (! empty($this->data['errorMessage'])) { + ?> +

+ Oops!
+ data['errorMessage']); ?> +

+ +
+

Manager Rescue Code

+

+ When you receive your code from your manager, enter it here. +

+

+ Enter code: +
+ + +
+ +

+ data['mfaOptions']) > 1): ?> +

+ You may also use: +

+
    + data['mfaOptions'] as $mfaOpt) { + if ($mfaOpt['type'] != 'manager') { + ?> +
  • + +
+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/templates/prompt-for-mfa-totp.php b/modules/mfa/templates/prompt-for-mfa-totp.php new file mode 100644 index 00000000..3603c8e9 --- /dev/null +++ b/modules/mfa/templates/prompt-for-mfa-totp.php @@ -0,0 +1,53 @@ +data['header'] = '2-Step Verification'; +$this->includeAtTemplateBase('includes/header.php'); + +if (! empty($this->data['errorMessage'])) { + ?> +

+ Oops!
+ data['errorMessage']); ?> +

+ +
+

Smartphone App

+

+ Enter 6-digit code: +
+ + +
+ +

+ data['mfaOptions']) > 1): ?> +

+ Don't have your smartphone app handy? You may also use: +

+
    + data['mfaOptions'] as $mfaOpt) { + if ($mfaOpt['type'] != 'totp') { + ?> +
  • + +
+ + data['managerEmail'])): ?> +

+ Can't use any of your 2-Step Verification options? + + Click here for assistance. +

+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/templates/prompt-for-mfa-webauthn.php b/modules/mfa/templates/prompt-for-mfa-webauthn.php new file mode 100644 index 00000000..12478937 --- /dev/null +++ b/modules/mfa/templates/prompt-for-mfa-webauthn.php @@ -0,0 +1,79 @@ +data['header'] = '2-Step Verification'; +$this->includeAtTemplateBase('includes/header.php'); +?> +data['supportsWebAuthn']): ?> + + + + +
+

USB Security Key

+ data['supportsWebAuthn']): ?> +

Please insert your security key and press its button.

+

+ + +
+ + + +

+ +

+ USB Security Keys are not supported in your current browser. + Please consider a more secure browser like + Google Chrome. +

+ + + data['mfaOptions']) > 1): ?> +

+ Don't have your security key handy? You may also use: +

+
    + data['mfaOptions'] as $mfaOpt) { + if ($mfaOpt['type'] != 'webauthn') { + ?> +
  • + +
+ + data['managerEmail'])): ?> +

+ Can't use any of your 2-Step Verification options? + + Click here for assistance. +

+ +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/templates/send-manager-mfa.php b/modules/mfa/templates/send-manager-mfa.php new file mode 100644 index 00000000..4a274ac6 --- /dev/null +++ b/modules/mfa/templates/send-manager-mfa.php @@ -0,0 +1,20 @@ +data['header'] = 'Send manager backup code'; +$this->includeAtTemplateBase('includes/header.php'); + +?> +

+ You can send a backup code to your manager to serve as an + additional 2-Step Verification option. + The email address on file (masked for privacy) is data['managerEmail'] ?> +

+
+ + +
+includeAtTemplateBase('includes/footer.php'); diff --git a/modules/mfa/www/low-on-backup-codes.php b/modules/mfa/www/low-on-backup-codes.php new file mode 100644 index 00000000..d8511ae5 --- /dev/null +++ b/modules/mfa/www/low-on-backup-codes.php @@ -0,0 +1,40 @@ +data['numBackupCodesRemaining'] = $state['numBackupCodesRemaining']; +$t->show(); + +$logger->info(sprintf( + 'mfa: Told Employee ID %s they are low on backup codes.', + $state['employeeId'] +)); diff --git a/modules/mfa/www/must-set-up-mfa.php b/modules/mfa/www/must-set-up-mfa.php new file mode 100644 index 00000000..a351c9bc --- /dev/null +++ b/modules/mfa/www/must-set-up-mfa.php @@ -0,0 +1,32 @@ +show(); + +$logger->info(sprintf( + 'mfa: Told Employee ID %s they they must set up MFA.', + $state['employeeId'] +)); diff --git a/modules/mfa/www/new-backup-codes.php b/modules/mfa/www/new-backup-codes.php new file mode 100644 index 00000000..01a4d01c --- /dev/null +++ b/modules/mfa/www/new-backup-codes.php @@ -0,0 +1,39 @@ +data['mfaSetupUrl'] = $state['mfaSetupUrl']; +$t->data['newBackupCodes'] = $state['newBackupCodes'] ?? []; +$t->show(); diff --git a/modules/mfa/www/out-of-backup-codes.php b/modules/mfa/www/out-of-backup-codes.php new file mode 100644 index 00000000..dc9e172c --- /dev/null +++ b/modules/mfa/www/out-of-backup-codes.php @@ -0,0 +1,42 @@ +data['hasOtherMfaOptions'] = $hasOtherMfaOptions; +$t->show(); + +$logger->info(sprintf( + 'mfa: Told Employee ID %s they are out of backup codes%s.', + $state['employeeId'], + $hasOtherMfaOptions ? '' : ' and must set up more' +)); diff --git a/modules/mfa/www/prompt-for-mfa.php b/modules/mfa/www/prompt-for-mfa.php new file mode 100644 index 00000000..d5f1cae3 --- /dev/null +++ b/modules/mfa/www/prompt-for-mfa.php @@ -0,0 +1,114 @@ +warning(json_encode([ + 'event' => 'MFA skipped due to valid remember-me cookie', + 'employeeId' => $state['employeeId'], + ])); + + unset($state['Attributes']['manager_email']); + + // This condition should never return + ProcessingChain::resumeProcessing($state); + throw new \Exception('Failed to resume processing auth proc chain.'); +} + +$mfaId = filter_input(INPUT_GET, 'mfaId'); +$userAgent = LoginBrowser::getUserAgent(); + +if (empty($mfaId)) { + $logger->critical(json_encode([ + 'event' => 'MFA ID missing in URL. Choosing one and doing a redirect.', + 'employeeId' => $state['employeeId'], + ])); + + // Pick an MFA ID and do a redirect to put that into the URL. + $mfaOption = Mfa::getMfaOptionToUse($mfaOptions, $userAgent); + $moduleUrl = SimpleSAML\Module::getModuleURL('mfa/prompt-for-mfa.php', [ + 'mfaId' => $mfaOption['id'], + 'StateId' => $stateId, + ]); + HTTP::redirectTrustedURL($moduleUrl); + return; +} +$mfaOption = Mfa::getMfaOptionById($mfaOptions, $mfaId); + +// If the user has submitted their MFA value... +if (filter_has_var(INPUT_POST, 'submitMfa')) { + $mfaSubmission = filter_input(INPUT_POST, 'mfaSubmission'); + if (substr($mfaSubmission, 0, 1) == '{') { + $mfaSubmission = json_decode($mfaSubmission, true); + } + + $rememberMe = filter_input(INPUT_POST, 'rememberMe') ?? false; + + // NOTE: This will only return if validation fails. + $errorMessage = Mfa::validateMfaSubmission( + $mfaId, + $state['employeeId'], + $mfaSubmission, + $state, + $rememberMe, + $logger, + $mfaOption['type'], + $state['rpOrigin'] + ); + + $logger->warning(json_encode([ + 'event' => 'MFA validation result: failed', + 'employeeId' => $state['employeeId'], + 'mfaType' => $mfaOption['type'], + 'error' => $errorMessage, + ])); +} + +$globalConfig = Configuration::getInstance(); + +$mfaTemplateToUse = Mfa::getTemplateFor($mfaOption['type']); + +$t = new Template($globalConfig, $mfaTemplateToUse); +$t->data['errorMessage'] = $errorMessage ?? null; +$t->data['mfaOption'] = $mfaOption; +$t->data['mfaOptions'] = $mfaOptions; +$t->data['stateId'] = $stateId; +$t->data['supportsWebAuthn'] = LoginBrowser::supportsWebAuthn($userAgent); +$t->data['managerEmail'] = $state['managerEmail']; +$t->show(); + +$logger->info(json_encode([ + 'event' => 'Prompted user for MFA', + 'employeeId' => $state['employeeId'], + 'mfaType' => $mfaOption['type'], +])); diff --git a/modules/mfa/www/send-manager-mfa.php b/modules/mfa/www/send-manager-mfa.php new file mode 100644 index 00000000..4eae0906 --- /dev/null +++ b/modules/mfa/www/send-manager-mfa.php @@ -0,0 +1,45 @@ + $stateId, + ]); + HTTP::redirectTrustedURL($moduleUrl); +} + +$globalConfig = Configuration::getInstance(); + +$t = new Template($globalConfig, 'mfa:send-manager-mfa.php'); +$t->data['stateId'] = $stateId; +$t->data['managerEmail'] = $state['managerEmail']; +$t->show(); + +$logger->info(json_encode([ + 'event' => 'offer to send manager code', + 'employeeId' => $state['employeeId'], +])); diff --git a/modules/mfa/www/simplewebauthn/LICENSE.md b/modules/mfa/www/simplewebauthn/LICENSE.md new file mode 100644 index 00000000..70730ac2 --- /dev/null +++ b/modules/mfa/www/simplewebauthn/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Matthew Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/modules/mfa/www/simplewebauthn/browser.js b/modules/mfa/www/simplewebauthn/browser.js new file mode 100644 index 00000000..8b1de96e --- /dev/null +++ b/modules/mfa/www/simplewebauthn/browser.js @@ -0,0 +1,2 @@ +/* [@simplewebauthn/browser] Version: 4.1.0 - Wednesday, September 1st, 2021, 9:11:50 AM */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).SimpleWebAuthnBrowser={})}(this,(function(e){"use strict";function t(e){const t=new Uint8Array(e);let n="";for(const e of t)n+=String.fromCharCode(e);return btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function n(e){const t=e.replace(/-/g,"+").replace(/_/g,"/"),n=(4-t.length%4)%4,r=t.padEnd(t.length+n,"="),o=atob(r),i=new ArrayBuffer(o.length),a=new Uint8Array(i);for(let e=0;e Date: Tue, 7 May 2024 15:57:28 +0800 Subject: [PATCH 21/92] use the local mfa module code and remove external module from composer --- composer.json | 1 - composer.lock | 110 +-------------------------------------------- docker-compose.yml | 7 +++ 3 files changed, 8 insertions(+), 110 deletions(-) diff --git a/composer.json b/composer.json index fa3b6c49..2f282785 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ "simplesamlphp/simplesamlphp": "^1.19.6", "simplesamlphp/composer-module-installer": "1.1.8", "silinternational/simplesamlphp-module-silauth": "^7.1.1", - "silinternational/simplesamlphp-module-mfa": "^5.2.1", "silinternational/ssp-utilities": "^1.1.0", "silinternational/simplesamlphp-module-material": "^8.1.1", "silinternational/simplesamlphp-module-sildisco": "^4.0.0", diff --git a/composer.lock b/composer.lock index 2ac04f9b..8fd70763 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": "b4f4532a5a284c0b780aaa48860974ae", + "content-hash": "b7ba67875da2080d961345975daa92cf", "packages": [ { "name": "aws/aws-crt-php", @@ -2984,60 +2984,6 @@ }, "time": "2023-06-12T17:37:14+00:00" }, - { - "name": "silinternational/simplesamlphp-module-mfa", - "version": "5.2.1", - "source": { - "type": "git", - "url": "https://github.com/silinternational/simplesamlphp-module-mfa.git", - "reference": "2179f28e5e72e1f14e27d10025cdac5e44b45398" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silinternational/simplesamlphp-module-mfa/zipball/2179f28e5e72e1f14e27d10025cdac5e44b45398", - "reference": "2179f28e5e72e1f14e27d10025cdac5e44b45398", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.2", - "silinternational/idp-id-broker-php-client": "^4.0.0", - "silinternational/php-env": "^2.1 || ^3.0", - "silinternational/psr3-adapters": "^1.1 || ^2.0 || ^3.0", - "simplesamlphp/simplesamlphp": "~1.17.7 || ~1.18.5 || ~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\\SspMfa\\": "src/", - "Sil\\SspMfa\\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 for MFA credentials (such as a TOTP code, etc.).", - "support": { - "issues": "https://github.com/silinternational/simplesamlphp-module-mfa/issues", - "source": "https://github.com/silinternational/simplesamlphp-module-mfa/tree/5.2.1" - }, - "time": "2023-06-15T13:38:51+00:00" - }, { "name": "silinternational/simplesamlphp-module-silauth", "version": "7.1.1", @@ -5040,60 +4986,6 @@ "abandoned": true, "time": "2022-11-28T16:34:29+00:00" }, - { - "name": "sinergi/browser-detector", - "version": "6.1.4", - "source": { - "type": "git", - "url": "https://github.com/sinergi/php-browser-detector.git", - "reference": "4927f7c2bedc48b68f183bd420aa3549c59e133b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sinergi/php-browser-detector/zipball/4927f7c2bedc48b68f183bd420aa3549c59e133b", - "reference": "4927f7c2bedc48b68f183bd420aa3549c59e133b", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.0 || ^9.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Sinergi\\BrowserDetector\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gabriel Bull", - "email": "me@gabrielbull.com" - }, - { - "name": "Chris Schuld" - } - ], - "description": "Detecting the user's browser, operating system and language.", - "keywords": [ - "browser", - "detection", - "language", - "operating system", - "os" - ], - "support": { - "issues": "https://github.com/sinergi/php-browser-detector/issues", - "source": "https://github.com/sinergi/php-browser-detector/tree/6.1.4" - }, - "abandoned": true, - "time": "2021-09-23T13:51:44+00:00" - }, { "name": "symfony/cache", "version": "v5.4.25", diff --git a/docker-compose.yml b/docker-compose.yml index cecf2a4b..e6508f7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: - ./development/ssp/run-debug.sh:/data/run-debug.sh # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview @@ -54,6 +55,7 @@ services: - ./features:/data/features - ./behat.yml:/data/behat.yml - ./tests:/data/tests + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: ["/data/run-tests.sh"] @@ -101,6 +103,7 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: /data/run-debug.sh @@ -144,6 +147,7 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' @@ -180,6 +184,7 @@ services: - ./development/idp2-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview command: /data/run.sh @@ -211,6 +216,7 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview ports: @@ -238,6 +244,7 @@ services: - ./development/sp2-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview ports: From 8262e5f84f15a2ad363f5c2bf94989b89939f0bd Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 7 May 2024 16:01:56 +0800 Subject: [PATCH 22/92] composer require silinternational/idp-id-broker-php-client --- composer.json | 3 ++- composer.lock | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 2f282785..0be4dad7 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "silinternational/simplesamlphp-module-sildisco": "^4.0.0", "silinternational/php-env": "^3.1.0", "silinternational/psr3-adapters": "^3.1", - "gettext/gettext": "^4.8@dev" + "gettext/gettext": "^4.8@dev", + "silinternational/idp-id-broker-php-client": "^4.3" }, "require-dev": { "behat/behat": "^3.8", diff --git a/composer.lock b/composer.lock index 8fd70763..8be4527f 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": "b7ba67875da2080d961345975daa92cf", + "content-hash": "f08b96212b20738531457507ce05ffe0", "packages": [ { "name": "aws/aws-crt-php", @@ -2794,16 +2794,16 @@ }, { "name": "silinternational/idp-id-broker-php-client", - "version": "4.3.1", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/silinternational/idp-id-broker-php-client.git", - "reference": "c05d01c0ed0666056249bdabd97c0392c99e9790" + "reference": "425955b2699110d6ff9a5d7cf1a15751cc1b8ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/silinternational/idp-id-broker-php-client/zipball/c05d01c0ed0666056249bdabd97c0392c99e9790", - "reference": "c05d01c0ed0666056249bdabd97c0392c99e9790", + "url": "https://api.github.com/repos/silinternational/idp-id-broker-php-client/zipball/425955b2699110d6ff9a5d7cf1a15751cc1b8ee9", + "reference": "425955b2699110d6ff9a5d7cf1a15751cc1b8ee9", "shasum": "" }, "require": { @@ -2847,9 +2847,9 @@ ], "support": { "issues": "https://github.com/silinternational/idp-id-broker-php-client/issues", - "source": "https://github.com/silinternational/idp-id-broker-php-client/tree/4.3.1" + "source": "https://github.com/silinternational/idp-id-broker-php-client/tree/4.3.2" }, - "time": "2023-06-19T12:59:34+00:00" + "time": "2024-02-28T19:30:43+00:00" }, { "name": "silinternational/php-env", From 294ff16fabe59c284a4f1c21656237e3e65220e9 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 7 May 2024 16:07:41 +0800 Subject: [PATCH 23/92] composer require sinergi/browser-detector --- composer.json | 3 ++- composer.lock | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0be4dad7..6fee2e44 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "silinternational/php-env": "^3.1.0", "silinternational/psr3-adapters": "^3.1", "gettext/gettext": "^4.8@dev", - "silinternational/idp-id-broker-php-client": "^4.3" + "silinternational/idp-id-broker-php-client": "^4.3", + "sinergi/browser-detector": "^6.1" }, "require-dev": { "behat/behat": "^3.8", diff --git a/composer.lock b/composer.lock index 8be4527f..e03bcdd0 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": "f08b96212b20738531457507ce05ffe0", + "content-hash": "69b009959cbdf313f49a8d5029e8bffe", "packages": [ { "name": "aws/aws-crt-php", @@ -4986,6 +4986,60 @@ "abandoned": true, "time": "2022-11-28T16:34:29+00:00" }, + { + "name": "sinergi/browser-detector", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sinergi/php-browser-detector.git", + "reference": "4927f7c2bedc48b68f183bd420aa3549c59e133b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sinergi/php-browser-detector/zipball/4927f7c2bedc48b68f183bd420aa3549c59e133b", + "reference": "4927f7c2bedc48b68f183bd420aa3549c59e133b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.0 || ^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sinergi\\BrowserDetector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com" + }, + { + "name": "Chris Schuld" + } + ], + "description": "Detecting the user's browser, operating system and language.", + "keywords": [ + "browser", + "detection", + "language", + "operating system", + "os" + ], + "support": { + "issues": "https://github.com/sinergi/php-browser-detector/issues", + "source": "https://github.com/sinergi/php-browser-detector/tree/6.1.4" + }, + "abandoned": true, + "time": "2021-09-23T13:51:44+00:00" + }, { "name": "symfony/cache", "version": "v5.4.25", From b87c9193df151fbb6b8ad331ac99755430586e8e Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 7 May 2024 16:08:37 +0800 Subject: [PATCH 24/92] move Assert, LoggerFactory, LoginBrowser to lib directory --- features/bootstrap/context/MfaContext.php | 6 +++--- features/fakes/FakeIdBrokerClient.php | 2 +- modules/mfa/{src => lib}/Assert.php | 2 +- modules/mfa/lib/Auth/Process/Mfa.php | 4 ++-- modules/mfa/{src => lib}/LoggerFactory.php | 4 ++-- modules/mfa/{src => lib}/LoginBrowser.php | 2 +- modules/mfa/www/low-on-backup-codes.php | 2 +- modules/mfa/www/must-set-up-mfa.php | 2 +- modules/mfa/www/new-backup-codes.php | 2 +- modules/mfa/www/out-of-backup-codes.php | 2 +- modules/mfa/www/prompt-for-mfa.php | 4 ++-- modules/mfa/www/send-manager-mfa.php | 2 +- 12 files changed, 17 insertions(+), 17 deletions(-) rename modules/mfa/{src => lib}/Assert.php (98%) rename modules/mfa/{src => lib}/LoggerFactory.php (94%) rename modules/mfa/{src => lib}/LoginBrowser.php (96%) diff --git a/features/bootstrap/context/MfaContext.php b/features/bootstrap/context/MfaContext.php index 020c49e8..3bc31f6c 100644 --- a/features/bootstrap/context/MfaContext.php +++ b/features/bootstrap/context/MfaContext.php @@ -1,5 +1,5 @@ Date: Tue, 7 May 2024 16:10:30 +0800 Subject: [PATCH 25/92] set up MfaContext for behat --- behat.yml | 2 +- .../bootstrap/{context => }/MfaContext.php | 37 +------------------ 2 files changed, 3 insertions(+), 36 deletions(-) rename features/bootstrap/{context => }/MfaContext.php (96%) diff --git a/behat.yml b/behat.yml index 053d4d3c..a5ecc748 100644 --- a/behat.yml +++ b/behat.yml @@ -11,7 +11,7 @@ default: contexts: [ 'FeatureContext' ] mfa_features: paths: [ '%paths.base%//features//mfa.feature' ] - contexts: [ 'FeatureContext' ] + contexts: [ 'MfaContext' ] profilereview_features: paths: [ '%paths.base%//features//profilereview.feature' ] contexts: [ 'ProfileReviewContext' ] diff --git a/features/bootstrap/context/MfaContext.php b/features/bootstrap/MfaContext.php similarity index 96% rename from features/bootstrap/context/MfaContext.php rename to features/bootstrap/MfaContext.php index 3bc31f6c..e8e2fa1a 100644 --- a/features/bootstrap/context/MfaContext.php +++ b/features/bootstrap/MfaContext.php @@ -1,12 +1,7 @@ driver = new GoutteDriver(); - $this->session = new Session($this->driver); - $this->session->start(); - } - + /** * Assert that the given page has a form that contains the given text. * From de593ea9467ed05d68050059f593dae46de902ba Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Thu, 9 May 2024 10:27:57 +0800 Subject: [PATCH 26/92] fixed (some of) the mfa tests --- composer.json | 5 +- composer.lock | 73 +- development/idp-local/config/authsources.php | 921 +++++++++++++++++- .../idp-local/metadata/saml20-idp-hosted.php | 14 + docker-compose.yml | 16 + features/bootstrap/MfaContext.php | 14 +- features/fakes/FakeIdBrokerClient.php | 2 +- features/mfa.feature | 64 +- modules/mfa/lib/Auth/Process/Mfa.php | 21 +- 9 files changed, 1046 insertions(+), 84 deletions(-) diff --git a/composer.json b/composer.json index 6fee2e44..b5a9031d 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,10 @@ "autoload": { "files": [ "vendor/yiisoft/yii2/Yii.php" - ] + ], + "psr-4": { + "Sil\\SspMfa\\Behat\\": "features/" + } }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index e03bcdd0..027ae04b 100644 --- a/composer.lock +++ b/composer.lock @@ -6690,16 +6690,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -6707,9 +6707,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -6753,7 +6750,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -6769,7 +6766,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php81", @@ -7795,16 +7792,16 @@ "packages-dev": [ { "name": "behat/behat", - "version": "v3.13.0", + "version": "v3.14.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "9dd7cdb309e464ddeab095cd1a5151c2dccba4ab" + "reference": "2a3832d9cb853a794af3a576f9e524ae460f3340" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/9dd7cdb309e464ddeab095cd1a5151c2dccba4ab", - "reference": "9dd7cdb309e464ddeab095cd1a5151c2dccba4ab", + "url": "https://api.github.com/repos/Behat/Behat/zipball/2a3832d9cb853a794af3a576f9e524ae460f3340", + "reference": "2a3832d9cb853a794af3a576f9e524ae460f3340", "shasum": "" }, "require": { @@ -7813,18 +7810,18 @@ "ext-mbstring": "*", "php": "^7.2 || ^8.0", "psr/container": "^1.0 || ^2.0", - "symfony/config": "^4.4 || ^5.0 || ^6.0", - "symfony/console": "^4.4 || ^5.0 || ^6.0", - "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", - "symfony/event-dispatcher": "^4.4 || ^5.0 || ^6.0", - "symfony/translation": "^4.4 || ^5.0 || ^6.0", - "symfony/yaml": "^4.4 || ^5.0 || ^6.0" + "symfony/config": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/console": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/translation": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^4.4 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { "herrera-io/box": "~1.6.1", "phpspec/prophecy": "^1.15", "phpunit/phpunit": "^8.5 || ^9.0", - "symfony/process": "^4.4 || ^5.0 || ^6.0", + "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0", "vimeo/psalm": "^4.8" }, "suggest": { @@ -7876,9 +7873,9 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.13.0" + "source": "https://github.com/Behat/Behat/tree/v3.14.0" }, - "time": "2023-04-18T15:40:53+00:00" + "time": "2023-12-09T13:55:02+00:00" }, { "name": "behat/gherkin", @@ -7945,26 +7942,28 @@ }, { "name": "behat/mink", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/minkphp/Mink.git", - "reference": "19e58905632e7cfdc5b2bafb9b950a3521af32c5" + "reference": "d8527fdf8785aad38455fb426af457ab9937aece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/minkphp/Mink/zipball/19e58905632e7cfdc5b2bafb9b950a3521af32c5", - "reference": "19e58905632e7cfdc5b2bafb9b950a3521af32c5", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/d8527fdf8785aad38455fb426af457ab9937aece", + "reference": "d8527fdf8785aad38455fb426af457ab9937aece", "shasum": "" }, "require": { "php": ">=7.2", - "symfony/css-selector": "^4.4 || ^5.0 || ^6.0" + "symfony/css-selector": "^4.4 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", "phpunit/phpunit": "^8.5.22 || ^9.5.11", - "symfony/error-handler": "^4.4 || ^5.0 || ^6.0", - "symfony/phpunit-bridge": "^5.4 || ^6.0" + "symfony/error-handler": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" }, "suggest": { "behat/mink-browserkit-driver": "fast headless driver for any app without JS emulation", @@ -8003,9 +8002,9 @@ ], "support": { "issues": "https://github.com/minkphp/Mink/issues", - "source": "https://github.com/minkphp/Mink/tree/v1.10.0" + "source": "https://github.com/minkphp/Mink/tree/v1.11.0" }, - "time": "2022-03-28T14:22:43+00:00" + "time": "2023-12-09T11:23:23+00:00" }, { "name": "behat/mink-extension", @@ -9533,16 +9532,16 @@ }, { "name": "symfony/css-selector", - "version": "v6.3.2", + "version": "v6.4.7", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "883d961421ab1709877c10ac99451632a3d6fa57" + "reference": "1c5d5c2103c3762aff27a27e1e2409e30a79083b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/883d961421ab1709877c10ac99451632a3d6fa57", - "reference": "883d961421ab1709877c10ac99451632a3d6fa57", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/1c5d5c2103c3762aff27a27e1e2409e30a79083b", + "reference": "1c5d5c2103c3762aff27a27e1e2409e30a79083b", "shasum": "" }, "require": { @@ -9578,7 +9577,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.3.2" + "source": "https://github.com/symfony/css-selector/tree/v6.4.7" }, "funding": [ { @@ -9594,7 +9593,7 @@ "type": "tidelift" } ], - "time": "2023-07-12T16:00:22+00:00" + "time": "2024-04-18T09:22:46+00:00" }, { "name": "symfony/translation", diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index f9fa9ee5..3af3dac7 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -1,5 +1,7 @@ ['distant_future@example.com'], 'employeeNumber' => ['11111'], 'cn' => ['DISTANT_FUTURE'], + 'mfa' => [ + 'prompt' => 'no', + ], 'schacExpiryDate' => [ gmdate('YmdHis\Z', strtotime('+6 months')), // Distant future ], @@ -31,6 +36,9 @@ 'mail' => ['near_future@example.com'], 'employeeNumber' => ['22222'], 'cn' => ['NEAR_FUTURE'], + 'mfa' => [ + 'prompt' => 'no', + ], 'schacExpiryDate' => [ gmdate('YmdHis\Z', strtotime('+1 day')), // Very soon ], @@ -42,6 +50,9 @@ 'mail' => ['already_past@example.com'], 'employeeNumber' => ['33333'], 'cn' => ['ALREADY_PAST'], + 'mfa' => [ + 'prompt' => 'no', + ], 'schacExpiryDate' => [ gmdate('YmdHis\Z', strtotime('-1 day')), // In the past ], @@ -61,6 +72,9 @@ 'mail' => ['invalid_exp@example.com'], 'employeeNumber' => ['55555'], 'cn' => ['INVALID_EXP'], + 'mfa' => [ + 'prompt' => 'no', + ], 'schacExpiryDate' => [ 'invalid' ], @@ -77,7 +91,7 @@ gmdate('YmdHis\Z', strtotime('+6 months')), ], 'mfa' => [ - 'prompt' => 'yes', + 'prompt' => 'no', 'add' => 'no', 'options' => [ [ @@ -130,7 +144,7 @@ gmdate('YmdHis\Z', strtotime('+6 months')), ], 'mfa' => [ - 'prompt' => 'yes', + 'prompt' => 'no', 'add' => 'no', 'options' => [ [ @@ -162,7 +176,7 @@ gmdate('YmdHis\Z', strtotime('+6 months')), ], 'mfa' => [ - 'prompt' => 'yes', + 'prompt' => 'no', 'add' => 'no', 'options' => [ [ @@ -199,5 +213,906 @@ ], 'profile_review' => 'yes' ], + 'no_mfa_needed:a' => [ + 'eduPersonPrincipalName' => ['NO_MFA_NEEDED@mfaidp'], + 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], + 'sn' => ['Needed'], + 'givenName' => ['No MFA'], + 'mail' => ['no_mfa_needed@example.com'], + 'employeeNumber' => ['11111'], + 'cn' => ['NO_MFA_NEEDED'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'no', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'must_set_up_mfa:a' => [ + 'eduPersonPrincipalName' => ['MUST_SET_UP_MFA@mfaidp'], + 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], + 'sn' => ['Set Up MFA'], + 'givenName' => ['Must'], + 'mail' => ['must_set_up_mfa@example.com'], + 'employeeNumber' => ['22222'], + 'cn' => ['MUST_SET_UP_MFA'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_backupcode:a' => [ + 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], + 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], + 'sn' => ['Backupcode'], + 'givenName' => ['Has'], + 'mail' => ['has_backupcode@example.com'], + 'employeeNumber' => ['33333'], + 'cn' => ['HAS_BACKUPCODE'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '7', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_backupcode_and_mgr:a' => [ + 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], + 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], + 'sn' => ['Backupcode'], + 'givenName' => ['Has'], + 'mail' => ['has_backupcode@example.com'], + 'employeeNumber' => ['33333'], + 'cn' => ['HAS_BACKUPCODE'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '7', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_totp:a' => [ + 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], + 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], + 'sn' => ['TOTP'], + 'givenName' => ['Has'], + 'mail' => ['has_totp@example.com'], + 'employeeNumber' => ['44444'], + 'cn' => ['HAS_TOTP'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '2', + 'type' => 'totp', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_totp_and_mgr:a' => [ + 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], + 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], + 'sn' => ['TOTP'], + 'givenName' => ['Has'], + 'mail' => ['has_totp@example.com'], + 'employeeNumber' => ['44444'], + 'cn' => ['HAS_TOTP'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '2', + 'type' => 'totp', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_webauthn:a' => [ + 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], + 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], + 'sn' => ['WebAuthn'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn@example.com'], + 'employeeNumber' => ['55555'], + 'cn' => ['HAS_WEBAUTHN'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '3', + 'type' => 'webauthn', + 'label' => 'Blue security key (work)', + 'created_utc' => '2017-10-24T20:40:57Z', + 'last_used_utc' => null, + 'data' => [ + // Response from "POST /webauthn/login" MFA API call. + ], + ], + ] + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_webauthn_and_mgr:a' => [ + 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], + 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], + 'sn' => ['WebAuthn'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn@example.com'], + 'employeeNumber' => ['55555'], + 'cn' => ['HAS_WEBAUTHN'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '3', + 'type' => 'webauthn', + 'data' => '', + ], + ] + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_all:a' => [ + 'eduPersonPrincipalName' => ['has_all@mfaidp'], + 'eduPersonTargetID' => ['77777777-7777-7777-7777-777777777777'], + 'sn' => ['All'], + 'givenName' => ['Has'], + 'mail' => ['has_all@example.com'], + 'employeeNumber' => ['777777'], + 'cn' => ['HAS_ALL'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '1', + 'type' => 'backupcode', + 'data' => [ + 'count' => 8, + ], + ], + [ + 'id' => '2', + 'type' => 'totp', + 'data' => '', + ], + [ + 'id' => '3', + 'type' => 'webauthn', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_rate_limited_mfa:a' => [ + 'eduPersonPrincipalName' => ['HAS_RATE_LIMITED_MFA@mfaidp'], + 'eduPersonTargetID' => ['88888888-8888-8888-8888-888888888888'], + 'sn' => ['Rate-Limited MFA'], + 'givenName' => ['Has'], + 'mail' => ['has_rate_limited_mfa@example.com'], + 'employeeNumber' => ['88888'], + 'cn' => ['HAS_RATE_LIMITED_MFA'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => 987, //FakeIdBrokerClient::RATE_LIMITED_MFA_ID, + 'type' => 'backupcode', + 'data' => [ + 'count' => 5, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_4_backupcodes:a' => [ + 'eduPersonPrincipalName' => ['HAS_4_BACKUPCODES@mfaidp'], + 'eduPersonTargetID' => ['99999999-9999-9999-9999-999999999999'], + 'sn' => ['Backupcodes'], + 'givenName' => ['Has 4'], + 'mail' => ['has_4_backupcodes@example.com'], + 'employeeNumber' => ['99999'], + 'cn' => ['HAS_4_BACKUPCODES'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '90', + 'type' => 'backupcode', + 'data' => [ + 'count' => 4, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_1_backupcode_only:a' => [ + 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_ONLY@mfaidp'], + 'eduPersonTargetID' => ['00000010-0010-0010-0010-000000000010'], + 'sn' => ['Only, And No Other MFA'], + 'givenName' => ['Has 1 Backupcode'], + 'mail' => ['has_1_backupcode_only@example.com'], + 'employeeNumber' => ['00010'], + 'cn' => ['HAS_1_BACKUPCODE_ONLY'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '100', + 'type' => 'backupcode', + 'data' => [ + 'count' => 1, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_1_backupcode_plus:a' => [ + 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_PLUS@mfaidp'], + 'eduPersonTargetID' => ['00000011-0011-0011-0011-000000000011'], + 'sn' => ['Plus Other MFA'], + 'givenName' => ['Has 1 Backupcode'], + 'mail' => ['has_1_backupcode_plus@example.com'], + 'employeeNumber' => ['00011'], + 'cn' => ['HAS_1_BACKUPCODE_PLUS'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '110', + 'type' => 'backupcode', + 'data' => [ + 'count' => 1, + ], + ], + [ + 'id' => '112', + 'type' => 'totp', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_webauthn_totp:a' => [ + 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], + 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], + 'sn' => ['WebAuthn And TOTP'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn_totp@example.com'], + 'employeeNumber' => ['00012'], + 'cn' => ['HAS_WEBAUTHN_TOTP'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '120', + 'type' => 'totp', + 'data' => '', + ], + [ + 'id' => '121', + 'type' => 'webauthn', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_webauthn_totp_and_mgr:a' => [ + 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], + 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], + 'sn' => ['WebAuthn And TOTP'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn_totp@example.com'], + 'employeeNumber' => ['00012'], + 'cn' => ['HAS_WEBAUTHN_TOTP'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '120', + 'type' => 'totp', + 'data' => '', + ], + [ + 'id' => '121', + 'type' => 'webauthn', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_webauthn_backupcodes:a' => [ + 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], + 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], + 'sn' => ['WebAuthn And Backup Codes'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn_backupcodes@example.com'], + 'employeeNumber' => ['00013'], + 'cn' => ['HAS_WEBAUTHN_BACKUPCODES'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '130', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + [ + 'id' => '131', + 'type' => 'webauthn', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_webauthn_backupcodes_and_mgr:a' => [ + 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], + 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], + 'sn' => ['WebAuthn And Backup Codes'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn_backupcodes@example.com'], + 'employeeNumber' => ['00013'], + 'cn' => ['HAS_WEBAUTHN_BACKUPCODES'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '130', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + [ + 'id' => '131', + 'type' => 'webauthn', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_webauthn_totp_backupcodes:a' => [ + 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], + 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], + 'sn' => ['WebAuthn, TOTP, And Backup Codes'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn_totp_backupcodes@example.com'], + 'employeeNumber' => ['00014'], + 'cn' => ['HAS_WEBAUTHN_TOTP_BACKUPCODES'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '140', + 'type' => 'totp', + 'data' => '', + ], + [ + 'id' => '141', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + [ + 'id' => '142', + 'type' => 'webauthn', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_webauthn_totp_backupcodes_and_mgr:a' => [ + 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], + 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], + 'sn' => ['WebAuthn, TOTP, And Backup Codes'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn_totp_backupcodes@example.com'], + 'employeeNumber' => ['00014'], + 'cn' => ['HAS_WEBAUTHN_TOTP_BACKUPCODES'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '140', + 'type' => 'totp', + 'data' => '', + ], + [ + 'id' => '141', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + [ + 'id' => '142', + 'type' => 'webauthn', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_mgr_code_webauthn_and_more_recently_used_totp:a' => [ + 'eduPersonPrincipalName' => ['has_mgr_code_webauthn_and_more_recently_used_totp@mfaidp'], + 'eduPersonTargetID' => ['00000114-0014-0014-0014-000000000014'], + 'sn' => ['Manager Code, WebAuthn, More Recently Used TOTP'], + 'givenName' => ['Has'], + 'mail' => ['has_mgr_code_webauthn_and_more_recently_used_totp@example.com'], + 'employeeNumber' => ['00114'], + 'cn' => ['HAS_MGR_CODE_WEBAUTHN_AND_MORE_RECENTLY_USED_TOTP'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '1140', + 'type' => 'totp', + 'last_used_utc' => '2011-01-01T00:00:00Z', + 'data' => '', + ], + [ + 'id' => '1141', + 'type' => 'webauthn', + 'last_used_utc' => '2000-01-01T00:00:00Z', + 'data' => '', + ], + [ + 'id' => '1142', + 'type' => 'manager', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_webauthn_and_more_recently_used_totp:a' => [ + 'eduPersonPrincipalName' => ['has_webauthn_and_more_recently_used_totp@mfaidp'], + 'eduPersonTargetID' => ['00000214-0014-0014-0014-000000000014'], + 'sn' => ['WebAuthn And More Recently Used TOTP'], + 'givenName' => ['Has'], + 'mail' => ['has_webauthn_and_more_recently_used_totp@example.com'], + 'employeeNumber' => ['00214'], + 'cn' => ['HAS_WEBAUTHN_AND_MORE_RECENTLY_USED_TOTP'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '2140', + 'type' => 'totp', + 'last_used_utc' => '2011-01-01T00:00:00Z', + 'data' => '', + ], + [ + 'id' => '2141', + 'type' => 'webauthn', + 'last_used_utc' => '2000-01-01T00:00:00Z', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_totp_and_more_recently_used_webauthn:a' => [ + 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_webauthn@mfaidp'], + 'eduPersonTargetID' => ['00000314-0014-0014-0014-000000000014'], + 'sn' => ['TOTP And More Recently Used Webauthn'], + 'givenName' => ['Has'], + 'mail' => ['has_totp_and_more_recently_used_webauthn@example.com'], + 'employeeNumber' => ['00314'], + 'cn' => ['HAS_TOTP_AND_MORE_RECENTLY_USED_WEBAUTHN'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '3140', + 'type' => 'totp', + 'last_used_utc' => '2000-01-01T00:00:00Z', + 'data' => '', + ], + [ + 'id' => '3141', + 'type' => 'webauthn', + 'last_used_utc' => '2011-01-01T00:00:00Z', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_totp_and_more_recently_used_backup_code:a' => [ + 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_backup_code@mfaidp'], + 'eduPersonTargetID' => ['00000414-0014-0014-0014-000000000014'], + 'sn' => ['TOTP And More Recently Used Backup Code'], + 'givenName' => ['Has'], + 'mail' => ['has_totp_and_more_recently_used_backup_code@example.com'], + 'employeeNumber' => ['00414'], + 'cn' => ['HAS_TOTP_AND_MORE_RECENTLY_USED_BACKUP_CODE'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '4140', + 'type' => 'totp', + 'last_used_utc' => '2000-01-01T00:00:00Z', + 'data' => '', + ], + [ + 'id' => '4141', + 'type' => 'backupcode', + 'last_used_utc' => '2011-01-01T00:00:00Z', + 'data' => [ + 'count' => 10, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_backup_code_and_more_recently_used_totp:a' => [ + 'eduPersonPrincipalName' => ['has_backup_code_and_more_recently_used_totp@mfaidp'], + 'eduPersonTargetID' => ['00000514-0014-0014-0014-000000000014'], + 'sn' => ['Backup Code And More Recently Used TOTP'], + 'givenName' => ['Has'], + 'mail' => ['has_backup_code_and_more_recently_used_totp@example.com'], + 'employeeNumber' => ['00514'], + 'cn' => ['HAS_BACKUP_CODE_AND_MORE_RECENTLY_USED_TOTP'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '5140', + 'type' => 'backupcode', + 'last_used_utc' => '2000-01-01T00:00:00Z', + 'data' => [ + 'count' => 10, + ], + ], + [ + 'id' => '5141', + 'type' => 'totp', + 'last_used_utc' => '2011-01-01T00:00:00Z', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_totp_backupcodes:a' => [ + 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], + 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], + 'sn' => ['TOTP And Backup Codes'], + 'givenName' => ['Has'], + 'mail' => ['has_totp_backupcodes@example.com'], + 'employeeNumber' => ['00015'], + 'cn' => ['HAS_TOTP_BACKUPCODES'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '150', + 'type' => 'totp', + 'data' => '', + ], + [ + 'id' => '151', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + ], + 'has_totp_backupcodes_and_mgr:a' => [ + 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], + 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], + 'sn' => ['TOTP And Backup Codes'], + 'givenName' => ['Has'], + 'mail' => ['has_totp_backupcodes@example.com'], + 'employeeNumber' => ['00015'], + 'cn' => ['HAS_TOTP_BACKUPCODES'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '150', + 'type' => 'totp', + 'data' => '', + ], + [ + 'id' => '151', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], + 'has_mgr_code:a' => [ + 'eduPersonPrincipalName' => ['has_mgr_code@mfaidp'], + 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], + 'sn' => ['Manager Code'], + 'givenName' => ['Has'], + 'mail' => ['has_mgr_code@example.com'], + 'employeeNumber' => ['00015'], + 'cn' => ['HAS_MGR_CODE'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'profile_review' => 'no', + 'mfa' => [ + 'prompt' => 'yes', + 'add' => 'no', + 'options' => [ + [ + 'id' => '151', + 'type' => 'backupcode', + 'data' => [ + 'count' => 10, + ], + ], + [ + 'id' => '152', + 'type' => 'manager', + 'data' => '', + ], + ], + ], + 'method' => [ + 'add' => 'no', + 'options' => [], + ], + 'manager_email' => ['manager@example.com'], + ], ], ]; diff --git a/development/idp-local/metadata/saml20-idp-hosted.php b/development/idp-local/metadata/saml20-idp-hosted.php index 588232b3..892df6cd 100644 --- a/development/idp-local/metadata/saml20-idp-hosted.php +++ b/development/idp-local/metadata/saml20-idp-hosted.php @@ -10,6 +10,7 @@ */ use Sil\Psr3Adapters\Psr3StdOutLogger; +use Sil\SspMfa\Behat\fakes\FakeIdBrokerClient; $metadata['http://ssp-idp1.local:8085'] = [ /* @@ -32,6 +33,18 @@ 'auth' => 'example-userpass', 'authproc' => [ + 10 => [ + 'class' => 'mfa:Mfa', + 'employeeIdAttr' => 'employeeNumber', + 'idBrokerAccessToken' => Env::get('ID_BROKER_ACCESS_TOKEN'), + 'idBrokerAssertValidIp' => Env::get('ID_BROKER_ASSERT_VALID_IP'), + 'idBrokerBaseUri' => Env::get('ID_BROKER_BASE_URI'), + 'idBrokerClientClass' => FakeIdBrokerClient::class, + 'idBrokerTrustedIpRanges' => Env::get('ID_BROKER_TRUSTED_IP_RANGES'), + 'idpDomainName' => Env::get('IDP_DOMAIN_NAME'), + 'mfaSetupUrl' => Env::get('MFA_SETUP_URL'), + 'loggerClass' => Psr3SamlLogger::class, + ], 15 => [ 'class' => 'expirychecker:ExpiryDate', 'accountNameAttr' => 'cn', @@ -54,4 +67,5 @@ // 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'][10]['mfaSetupUrl'] = Env::get('MFA_SETUP_URL_FOR_TESTS'); $metadata['http://ssp-idp1.local']['authproc'][30]['profileUrl'] = Env::get('PROFILE_URL_FOR_TESTS'); diff --git a/docker-compose.yml b/docker-compose.yml index e6508f7a..1a0f6eef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,6 +44,11 @@ services: environment: - COMPOSER_CACHE_DIR=/composer - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - MFA_SETUP_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=b + - SECRET_SALT=abc123 + - IDP_NAME=x volumes: - ./composer.json:/data/composer.json - ./composer.lock:/data/composer.lock @@ -146,6 +151,9 @@ services: # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh + # Include the features folder (for the FakeIdBrokerClient class) + - ./features:/data/features + # Local modules - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker @@ -158,6 +166,14 @@ services: ADMIN_PASS: "a" SECRET_SALT: "h57fjemb&dn^nsJFGNjweJ" IDP_NAME: "IDP 1" + IDP_DOMAIN_NAME: "mfaidp" + ID_BROKER_ACCESS_TOKEN: "dummy" + ID_BROKER_ASSERT_VALID_IP: "false" + ID_BROKER_BASE_URI: "dummy" + ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" + MFA_SETUP_URL: "http://ssp-hub-sp1:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL_FOR_TESTS: "http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub" + REMEMBER_ME_SECRET: "12345" 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" diff --git a/features/bootstrap/MfaContext.php b/features/bootstrap/MfaContext.php index e8e2fa1a..29fc5b71 100644 --- a/features/bootstrap/MfaContext.php +++ b/features/bootstrap/MfaContext.php @@ -4,7 +4,7 @@ use Behat\Mink\Exception\ElementNotFoundException; use PHPUnit\Framework\Assert; use Sil\PhpEnv\Env; -use SimpleSAML\Module\mfa\Behat\fakes\FakeIdBrokerClient; +use Sil\SspMfa\Behat\fakes\FakeIdBrokerClient; use SimpleSAML\Module\mfa\LoginBrowser; /** @@ -12,8 +12,6 @@ */ class MfaContext extends FeatureContext { - protected $nonPwManagerUrl = 'http://mfasp/module.php/core/authenticate.php?as=mfa-idp-no-port'; - protected $username = null; protected $password = null; @@ -93,7 +91,6 @@ protected function getSubmitMfaButton($page) */ public function iLogin() { - $this->session->visit($this->nonPwManagerUrl); $page = $this->session->getPage(); try { $page->fillField('username', $this->username); @@ -327,11 +324,6 @@ protected function pageContainsElementWithText($cssSelector, $text) } return false; } - - protected function clickLink($text) - { - $this->session->getPage()->clickLink($text); - } /** * @When I submit an incorrect backup code @@ -549,7 +541,7 @@ public function theUsersBrowserSupportsUf() 'Update USER_AGENT_WITH_WEBAUTHN_SUPPORT to a User Agent with WebAuthn support' ); - $this->driver->getClient()->setServerParameter('HTTP_USER_AGENT', $userAgentWithWebAuthn); +// $this->driver->getClient()->setServerParameter('HTTP_USER_AGENT', $userAgentWithWebAuthn); } /** @@ -683,7 +675,7 @@ public function theUsersBrowserDoesNotSupportUf() 'Update USER_AGENT_WITHOUT_WEBAUTHN_SUPPORT to a User Agent without WebAuthn support' ); - $this->driver->getClient()->setServerParameter('HTTP_USER_AGENT', $userAgentWithoutWebAuthn); +// $this->driver->getClient()->setServerParameter('HTTP_USER_AGENT', $userAgentWithoutWebAuthn); } /** diff --git a/features/fakes/FakeIdBrokerClient.php b/features/fakes/FakeIdBrokerClient.php index cb113e83..3f339b7c 100644 --- a/features/fakes/FakeIdBrokerClient.php +++ b/features/fakes/FakeIdBrokerClient.php @@ -1,5 +1,5 @@ @@ -208,20 +212,22 @@ Feature: Prompt for MFA credentials When I click the Request Assistance link Then there should be a way to request a manager code - Scenario: Submit a code sent to my manager at an earlier time - Given I provide credentials that have a manager code - And I login - When I submit the correct manager code - Then I should end up at my intended destination - - Scenario: Submit a correct manager code - Given I provide credentials that have backup codes - And the user has a manager email - And I login - And I click the Request Assistance link - And I click the Send a code link - When I submit the correct manager code - Then I should end up at my intended destination +# Scenario: Submit a code sent to my manager at an earlier time +# Given I provide credentials that have a manager code +# And I login +# When I submit the correct manager code +## TODO: add a step here because using a manager code forces profile review +# Then I should end up at my intended destination + +# Scenario: Submit a correct manager code +# Given I provide credentials that have backup codes +# And the user has a manager email +# And I login +# And I click the Request Assistance link +# And I click the Send a code link +# When I submit the correct manager code +## TODO: add a step here because using a manager code forces profile review +# Then I should end up at my intended destination Scenario: Submit an incorrect manager code Given I provide credentials that have backup codes diff --git a/modules/mfa/lib/Auth/Process/Mfa.php b/modules/mfa/lib/Auth/Process/Mfa.php index d207a8da..ecf95aa8 100644 --- a/modules/mfa/lib/Auth/Process/Mfa.php +++ b/modules/mfa/lib/Auth/Process/Mfa.php @@ -582,7 +582,16 @@ public function process(&$state) $state, $this->mfaSetupUrl ); - + + $this->logger->debug(json_encode([ + 'module' => 'mfa', + 'event' => 'process', + 'mfa' => $mfa, + 'isHeadedToMfaSetupUrl' => $isHeadedToMfaSetupUrl, + 'employeeId' => $employeeId, + ])); + + // Record to the state what logger class to use. $state['loggerClass'] = $this->loggerClass; @@ -666,8 +675,16 @@ protected function redirectToMfaPrompt(&$state, $employeeId, $mfaOptions) $id = State::saveState($state, self::STAGE_SENT_TO_MFA_PROMPT); $url = Module::getModuleURL('mfa/prompt-for-mfa.php'); + $userAgent = LoginBrowser::getUserAgent(); + $webauthnSupport = LoginBrowser::supportsWebAuthn($userAgent); + + $this->logger->debug(json_encode([ + 'event' => 'check browser', + 'user_agent' => $userAgent, + 'webauthn_support' => $webauthnSupport, + ])); - $mfaOption = self::getMfaOptionToUse($mfaOptions, LoginBrowser::getUserAgent()); + $mfaOption = self::getMfaOptionToUse($mfaOptions, $userAgent); HTTP::redirectTrustedURL($url, [ 'mfaId' => $mfaOption['id'], From f9ed4503c6f951366ac2cceeb6952852fbec0819 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 10 May 2024 12:18:04 +0800 Subject: [PATCH 27/92] copy docker-compose.yml changes into actions-services.yml --- actions-services.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/actions-services.yml b/actions-services.yml index 0fc10d60..d5f9985d 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -10,6 +10,11 @@ services: - test-browser environment: - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - MFA_SETUP_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=b + - SECRET_SALT=abc123 + - IDP_NAME=x volumes: - ./dockerbuild/run-integration-tests.sh:/data/run-integration-tests.sh - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -75,12 +80,23 @@ services: # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh + + # Include the features folder (for the FakeIdBrokerClient class) + - ./features:/data/features command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' environment: ADMIN_EMAIL: "john_doe@there.com" ADMIN_PASS: "a" SECRET_SALT: "not-secret-h57fjemb&dn^nsJFGNjweJ" IDP_NAME: "IDP 1" + IDP_DOMAIN_NAME: "mfaidp" + ID_BROKER_ACCESS_TOKEN: "dummy" + ID_BROKER_ASSERT_VALID_IP: "false" + ID_BROKER_BASE_URI: "dummy" + ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" + MFA_SETUP_URL: "http://ssp-hub-sp1:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL_FOR_TESTS: "http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub" + REMEMBER_ME_SECRET: "12345" 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" From e8077326796147d406286bf8369f664f12fb82ab Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 10 May 2024 15:34:54 +0800 Subject: [PATCH 28/92] fixed mfa.feature:19 test scenario by defining a new pwmanager service --- actions-services.yml | 29 ++++++++++++++-- .../sp-local/config/authsources-pwmanager.php | 28 ++++++++++++++++ docker-compose.yml | 33 ++++++++++++++++--- features/bootstrap/MfaContext.php | 4 +-- features/mfa.feature | 12 +++---- 5 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 development/sp-local/config/authsources-pwmanager.php diff --git a/actions-services.yml b/actions-services.yml index d5f9985d..f8a8d562 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -7,10 +7,11 @@ services: - ssp-idp1.local - ssp-idp2.local - ssp-sp1.local + - pwmanager.local - test-browser environment: - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub - - MFA_SETUP_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - MFA_SETUP_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=b - SECRET_SALT=abc123 @@ -94,8 +95,8 @@ services: ID_BROKER_ASSERT_VALID_IP: "false" ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" - MFA_SETUP_URL: "http://ssp-hub-sp1:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" - MFA_SETUP_URL_FOR_TESTS: "http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub" + MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" REMEMBER_ME_SECRET: "12345" 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" @@ -155,3 +156,25 @@ services: SHOW_SAML_ERRORS: "true" SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" + + pwmanager.local: + image: silintl/ssp-base:develop + volumes: + # Utilize custom certs + - ./development/sp-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp-local/config/authsources-pwmanager.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp1 + - IDP_NAME=THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED + - SECRET_SALT=NOT-a-secret-k49fjfkw73hjf9t87wjiw + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + - THEME_USE=default diff --git a/development/sp-local/config/authsources-pwmanager.php b/development/sp-local/config/authsources-pwmanager.php new file mode 100644 index 00000000..ea9c8ab0 --- /dev/null +++ b/development/sp-local/config/authsources-pwmanager.php @@ -0,0 +1,28 @@ + [ + // The default is to use core:AdminPassword, but it can be replaced with + // any authentication source. + + 'core:AdminPassword', + ], + + 'mfa-idp' => [ + 'saml:SP', + 'entityID' => 'http://pwmanager.local:8083', + 'idp' => 'http://ssp-idp1.local:8085', + 'discoURL' => null, + 'NameIDPolicy' => "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + ], + + 'mfa-idp-no-port' => [ + 'saml:SP', + 'entityID' => 'http://pwmanager.local', + 'idp' => 'http://ssp-idp1.local', + 'discoURL' => null, + 'NameIDPolicy' => "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + ], +]; diff --git a/docker-compose.yml b/docker-compose.yml index 1a0f6eef..af547cf8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,11 +40,12 @@ services: - ssp-idp1.local - ssp-idp2.local - ssp-sp1.local + - pwmanager.local - test-browser environment: - COMPOSER_CACHE_DIR=/composer - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub - - MFA_SETUP_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - MFA_SETUP_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=b - SECRET_SALT=abc123 @@ -171,8 +172,8 @@ services: ID_BROKER_ASSERT_VALID_IP: "false" ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" - MFA_SETUP_URL: "http://ssp-hub-sp1:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" - MFA_SETUP_URL_FOR_TESTS: "http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub" + MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" REMEMBER_ME_SECRET: "12345" 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" @@ -273,7 +274,31 @@ services: SHOW_SAML_ERRORS: "true" SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" - + + pwmanager.local: + image: silintl/ssp-base:develop + volumes: + # Utilize custom certs + - ./development/sp-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp-local/config/authsources-pwmanager.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + ports: + - "8083:80" + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp1 + - IDP_NAME=THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED + - SECRET_SALT=NOT-a-secret-k49fjfkw73hjf9t87wjiw + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + - THEME_USE=default + networks: default: driver: bridge diff --git a/features/bootstrap/MfaContext.php b/features/bootstrap/MfaContext.php index 29fc5b71..b2a80652 100644 --- a/features/bootstrap/MfaContext.php +++ b/features/bootstrap/MfaContext.php @@ -419,9 +419,9 @@ public function thereShouldNotBeAWayToContinueToMyIntendedDestination() */ public function iShouldNotBeAbleToGetToMyIntendedDestination() { - $this->session->visit($this->nonPwManagerUrl); + $this->session->visit(self::SP1_LOGIN_PAGE); Assert::assertStringStartsNotWith( - $this->nonPwManagerUrl, + self::SP1_LOGIN_PAGE, $this->session->getCurrentUrl(), 'Failed to prevent me from getting to SPs other than the MFA setup URL' ); diff --git a/features/mfa.feature b/features/mfa.feature index f194122c..e4a90706 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -16,12 +16,12 @@ Feature: Prompt for MFA credentials And there should be a way to go set up MFA now And there should NOT be a way to continue to my intended destination -# Scenario: Following the requirement to go set up MFA -# Given I provide credentials that need MFA but have no MFA options available -# And I login -# When I click the set-up-MFA button -# Then I should end up at the mfa-setup URL -# And I should NOT be able to get to my intended destination + Scenario: Following the requirement to go set up MFA + Given I provide credentials that need MFA but have no MFA options available + And I login + When I click the set-up-MFA button + Then I should end up at the mfa-setup URL + And I should NOT be able to get to my intended destination Scenario: Needs MFA, has backup code option available Given I provide credentials that need MFA and have backup codes available From 53067785dd26492dde7da47e7f40573983fa6ab4 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 10 May 2024 15:46:54 +0800 Subject: [PATCH 29/92] fixed profilereview.feature:24 test scenario using pwmanager service --- actions-services.yml | 6 +++--- docker-compose.yml | 6 +++--- dockerbuild/run-integration-tests.sh | 6 +++--- features/profilereview.feature | 3 --- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/actions-services.yml b/actions-services.yml index f8a8d562..85f6bae3 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -10,7 +10,7 @@ services: - pwmanager.local - test-browser environment: - - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - MFA_SETUP_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=b @@ -98,8 +98,8 @@ services: MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" MFA_SETUP_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" REMEMBER_ME_SECRET: "12345" - 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" + PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" THEME_USE: "default" diff --git a/docker-compose.yml b/docker-compose.yml index af547cf8..32727eee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,7 @@ services: - test-browser environment: - COMPOSER_CACHE_DIR=/composer - - PROFILE_URL_FOR_TESTS=http://ssp-sp1.local/module.php/core/authenticate.php?as=ssp-hub + - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - MFA_SETUP_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=b @@ -175,8 +175,8 @@ services: MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" MFA_SETUP_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" REMEMBER_ME_SECRET: "12345" - 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" + PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" THEME_USE: "default" diff --git a/dockerbuild/run-integration-tests.sh b/dockerbuild/run-integration-tests.sh index c46b02c5..71d8c006 100755 --- a/dockerbuild/run-integration-tests.sh +++ b/dockerbuild/run-integration-tests.sh @@ -6,9 +6,9 @@ set -x cd /data export COMPOSER_ALLOW_SUPERUSER=1; composer install -whenavail "ssp-hub.local" 80 10 echo Hub ready -whenavail "ssp-idp1.local" 80 10 echo IDP 1 ready -whenavail "ssp-sp1.local" 80 10 echo SP 1 ready +whenavail "ssp-hub.local" 80 15 echo Hub ready +whenavail "ssp-idp1.local" 80 5 echo IDP 1 ready +whenavail "ssp-sp1.local" 80 5 echo SP 1 ready ./vendor/bin/behat \ --no-interaction \ diff --git a/features/profilereview.feature b/features/profilereview.feature index 56643e9d..b378e712 100644 --- a/features/profilereview.feature +++ b/features/profilereview.feature @@ -25,9 +25,6 @@ Feature: Prompt to review profile information 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: From 7ef8d2184c35b7e5942c3039402972684222e26a Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 10 May 2024 15:50:57 +0800 Subject: [PATCH 30/92] remove MFA_SETUP_URL_FOR_TESTS and use PROFILE_URL_FOR_TESTS instead --- actions-services.yml | 2 -- development/idp-local/metadata/saml20-idp-hosted.php | 2 +- docker-compose.yml | 2 -- features/bootstrap/MfaContext.php | 4 ++-- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/actions-services.yml b/actions-services.yml index 85f6bae3..32bfe9d2 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -11,7 +11,6 @@ services: - test-browser environment: - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - - MFA_SETUP_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=b - SECRET_SALT=abc123 @@ -96,7 +95,6 @@ services: ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" - MFA_SETUP_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" REMEMBER_ME_SECRET: "12345" PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" diff --git a/development/idp-local/metadata/saml20-idp-hosted.php b/development/idp-local/metadata/saml20-idp-hosted.php index 892df6cd..e9b6b14a 100644 --- a/development/idp-local/metadata/saml20-idp-hosted.php +++ b/development/idp-local/metadata/saml20-idp-hosted.php @@ -67,5 +67,5 @@ // 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'][10]['mfaSetupUrl'] = Env::get('MFA_SETUP_URL_FOR_TESTS'); +$metadata['http://ssp-idp1.local']['authproc'][10]['mfaSetupUrl'] = Env::get('PROFILE_URL_FOR_TESTS'); $metadata['http://ssp-idp1.local']['authproc'][30]['profileUrl'] = Env::get('PROFILE_URL_FOR_TESTS'); diff --git a/docker-compose.yml b/docker-compose.yml index 32727eee..178786d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,7 +45,6 @@ services: environment: - COMPOSER_CACHE_DIR=/composer - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - - MFA_SETUP_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=b - SECRET_SALT=abc123 @@ -173,7 +172,6 @@ services: ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" - MFA_SETUP_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" REMEMBER_ME_SECRET: "12345" PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" diff --git a/features/bootstrap/MfaContext.php b/features/bootstrap/MfaContext.php index b2a80652..ced88bce 100644 --- a/features/bootstrap/MfaContext.php +++ b/features/bootstrap/MfaContext.php @@ -394,8 +394,8 @@ public function iClickTheSetUpMfaButton() */ public function iShouldEndUpAtTheMfaSetupUrl() { - $mfaSetupUrl = Env::get('MFA_SETUP_URL_FOR_TESTS'); - Assert::assertNotEmpty($mfaSetupUrl, 'No MFA_SETUP_URL_FOR_TESTS provided'); + $mfaSetupUrl = Env::get('PROFILE_URL_FOR_TESTS'); + Assert::assertNotEmpty($mfaSetupUrl, 'No PROFILE_URL_FOR_TESTS provided'); $currentUrl = $this->session->getCurrentUrl(); Assert::assertStringStartsWith( $mfaSetupUrl, From d72cd8b322d5fce79f8c5af56118de9800e021f8 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 10 May 2024 18:40:46 +0800 Subject: [PATCH 31/92] add missing step to mfa scenarios that prompt a review --- features/mfa.feature | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/features/mfa.feature b/features/mfa.feature index e4a90706..a61a61ab 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -212,22 +212,22 @@ Feature: Prompt for MFA credentials When I click the Request Assistance link Then there should be a way to request a manager code -# Scenario: Submit a code sent to my manager at an earlier time -# Given I provide credentials that have a manager code -# And I login -# When I submit the correct manager code -## TODO: add a step here because using a manager code forces profile review -# Then I should end up at my intended destination - -# Scenario: Submit a correct manager code -# Given I provide credentials that have backup codes -# And the user has a manager email -# And I login -# And I click the Request Assistance link -# And I click the Send a code link -# When I submit the correct manager code -## TODO: add a step here because using a manager code forces profile review -# Then I should end up at my intended destination + Scenario: Submit a code sent to my manager at an earlier time + Given I provide credentials that have a manager code + And I login + When I submit the correct manager code + And I click the remind-me-later button + Then I should end up at my intended destination + + Scenario: Submit a correct manager code + Given I provide credentials that have backup codes + And the user has a manager email + And I login + And I click the Request Assistance link + And I click the Send a code link + When I submit the correct manager code + And I click the remind-me-later button + Then I should end up at my intended destination Scenario: Submit an incorrect manager code Given I provide credentials that have backup codes From c13c98c91bb119c4cc9e6bd002c988996abe9c73 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 13 May 2024 10:34:36 +0800 Subject: [PATCH 32/92] copy relevant README paragraphs from the simplesamlphp-module-mfa repo --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/README.md b/README.md index 4986cd25..8523efe3 100644 --- a/README.md +++ b/README.md @@ -171,3 +171,78 @@ can be autoloaded, to use as the logger within ExpiryDate. This is adapted from the `ssp-iidp-expirycheck` and `expirycheck` modules. Thanks to Alex Mihičinac, Steve Moitozo, and Steve Bagwell for the initial work they did on those two modules. + +### Multi-Factor Authentication (MFA) simpleSAMLphp Module +A simpleSAMLphp module for prompting the user for MFA credentials (such as a +TOTP code, etc.). + +This mfa module is implemented as an Authentication Processing Filter, +or AuthProc. That means it can be configured in the global config.php file or +the SP remote or IdP hosted metadata. + +It is recommended to run the mfa module at the IdP, and configure the +filter to run before all the other filters you may have enabled. + +#### How to use the module + +You will need to set filter parameters in your config. We recommend adding +them to the `'authproc'` array in your `metadata/saml20-idp-hosted.php` file. + +Example (for `metadata/saml20-idp-hosted.php`): + + use Sil\PhpEnv\Env; + use Sil\Psr3Adapters\Psr3SamlLogger; + + // ... + + 'authproc' => [ + 10 => [ + // Required: + 'class' => 'mfa:Mfa', + 'employeeIdAttr' => 'employeeNumber', + 'idBrokerAccessToken' => Env::get('ID_BROKER_ACCESS_TOKEN'), + 'idBrokerAssertValidIp' => Env::get('ID_BROKER_ASSERT_VALID_IP'), + 'idBrokerBaseUri' => Env::get('ID_BROKER_BASE_URI'), + 'idBrokerTrustedIpRanges' => Env::get('ID_BROKER_TRUSTED_IP_RANGES'), + 'idpDomainName' => Env::get('IDP_DOMAIN_NAME'), + 'mfaSetupUrl' => Env::get('MFA_SETUP_URL'), + + // Optional: + 'loggerClass' => Psr3SamlLogger::class, + ], + + // ... + ], + +The `employeeIdAttr` parameter represents the SAML attribute name which has +the user's Employee ID stored in it. In certain situations, this may be +displayed to the user, as well as being used in log messages. + +The `loggerClass` parameter specifies the name of a PSR-3 compatible class that +can be autoloaded, to use as the logger within ExpiryDate. + +The `mfaSetupUrl` parameter is for the URL of where to send the user if they +want/need to set up MFA. + +The `idpDomainName` parameter is used to assemble the Relying Party Origin +(RP Origin) for WebAuthn MFA options. + +#### Why use an AuthProc for MFA? +Based on... + +- the existence of multiple other simpleSAMLphp modules used for MFA and + implemented as AuthProcs, +- implementing my solution as an AuthProc and having a number of tests that all + confirm that it is working as desired, and +- a discussion in the SimpleSAMLphp mailing list about this: + https://groups.google.com/d/msg/simplesamlphp/ocQols0NCZ8/RL_WAcryBwAJ + +... it seems sufficiently safe to implement MFA using a simpleSAMLphp AuthProc. + +For more of the details, please see this Stack Overflow Q&A: +https://stackoverflow.com/q/46566014/3813891 + +#### Acknowledgements +This is adapted from the `silinternational/simplesamlphp-module-mfa` +module, which itself is adapted from other modules. Thanks to all those who +contributed to that work. From 14bb76670f9a60451b65fc363c92437e061f3ac0 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 13 May 2024 10:50:06 +0800 Subject: [PATCH 33/92] copy content from the simplesamlphp-module-silauth module repo https://github.com/silinternational/simplesamlphp-module-silauth --- README.md | 80 +++ actions-services.yml | 52 ++ behat.yml | 6 + docker-compose.yml | 47 ++ features/bootstrap/LoginContext.php | 643 ++++++++++++++++++ features/bootstrap/StatusContext.php | 56 ++ features/login.feature | 207 ++++++ features/status.feature | 9 + local.broker.env.dist | 18 + local.env.dist | 12 + .../dictionaries/error.definition.json | 50 ++ modules/silauth/lib/Auth/Source/SilAuth.php | 160 +++++ modules/silauth/src/auth/AuthError.php | 72 ++ modules/silauth/src/auth/Authenticator.php | 363 ++++++++++ modules/silauth/src/auth/IdBroker.php | 109 +++ .../src/behaviors/CreatedAtUtcBehavior.php | 22 + modules/silauth/src/captcha/Captcha.php | 29 + modules/silauth/src/config/ConfigManager.php | 110 +++ modules/silauth/src/config/ssp-config.php | 21 + modules/silauth/src/config/yii2-config.php | 72 ++ modules/silauth/src/csrf/CsrfProtector.php | 81 +++ modules/silauth/src/http/Request.php | 212 ++++++ .../M161213135750CreateInitialTables.php | 62 ++ .../M161213150831SwitchToUtcForDateTimes.php | 22 + .../M170214141109CreateFailedLoginsTable.php | 39 ++ .../M170214145629RemoveOldTables.php | 30 + .../M170215141724SplitFailedLoginsTable.php | 44 ++ .../src/models/FailedLoginIpAddress.php | 159 +++++ .../src/models/FailedLoginIpAddressBase.php | 47 ++ .../src/models/FailedLoginUsername.php | 140 ++++ .../src/models/FailedLoginUsernameBase.php | 47 ++ modules/silauth/src/rebuildbasemodels.sh | 14 + modules/silauth/src/saml/User.php | 61 ++ modules/silauth/src/system/System.php | 96 +++ modules/silauth/src/tests/bootstrap.php | 14 + .../src/tests/fakes/FakeFailedIdBroker.php | 21 + .../silauth/src/tests/fakes/FakeIdBroker.php | 47 ++ .../src/tests/fakes/FakeInvalidIdBroker.php | 18 + .../tests/fakes/FakeSuccessfulIdBroker.php | 41 ++ modules/silauth/src/tests/phpunit.xml | 4 + .../src/tests/unit/auth/AuthenticatorTest.php | 97 +++ .../tests/unit/captcha/DummyFailedCaptcha.php | 13 + .../unit/captcha/DummySuccessfulCaptcha.php | 13 + .../tests/unit/config/ConfigManagerTest.php | 60 ++ .../src/tests/unit/csrf/CsrfProtectorTest.php | 26 + .../src/tests/unit/csrf/FakeSession.php | 41 ++ .../src/tests/unit/http/DummyRequest.php | 31 + .../src/tests/unit/http/RequestTest.php | 51 ++ .../unit/models/FailedLoginIpAddressTest.php | 189 +++++ .../unit/models/FailedLoginUsernameTest.php | 188 +++++ .../silauth/src/tests/unit/text/TextTest.php | 42 ++ .../src/tests/unit/time/UtcTimeTest.php | 167 +++++ .../src/tests/unit/time/WaitTimeTest.php | 73 ++ modules/silauth/src/text/Text.php | 34 + modules/silauth/src/time/UtcTime.php | 120 ++++ modules/silauth/src/time/WaitTime.php | 73 ++ .../silauth/src/traits/LoggerAwareTrait.php | 29 + modules/silauth/src/yii | 43 ++ modules/silauth/www/loginuserpass.php | 108 +++ modules/silauth/www/status.php | 32 + 60 files changed, 4767 insertions(+) create mode 100644 features/bootstrap/LoginContext.php create mode 100644 features/bootstrap/StatusContext.php create mode 100644 features/login.feature create mode 100644 features/status.feature create mode 100755 local.broker.env.dist create mode 100644 modules/silauth/dictionaries/error.definition.json create mode 100644 modules/silauth/lib/Auth/Source/SilAuth.php create mode 100644 modules/silauth/src/auth/AuthError.php create mode 100644 modules/silauth/src/auth/Authenticator.php create mode 100644 modules/silauth/src/auth/IdBroker.php create mode 100644 modules/silauth/src/behaviors/CreatedAtUtcBehavior.php create mode 100644 modules/silauth/src/captcha/Captcha.php create mode 100644 modules/silauth/src/config/ConfigManager.php create mode 100644 modules/silauth/src/config/ssp-config.php create mode 100644 modules/silauth/src/config/yii2-config.php create mode 100644 modules/silauth/src/csrf/CsrfProtector.php create mode 100644 modules/silauth/src/http/Request.php create mode 100644 modules/silauth/src/migrations/M161213135750CreateInitialTables.php create mode 100644 modules/silauth/src/migrations/M161213150831SwitchToUtcForDateTimes.php create mode 100644 modules/silauth/src/migrations/M170214141109CreateFailedLoginsTable.php create mode 100644 modules/silauth/src/migrations/M170214145629RemoveOldTables.php create mode 100644 modules/silauth/src/migrations/M170215141724SplitFailedLoginsTable.php create mode 100644 modules/silauth/src/models/FailedLoginIpAddress.php create mode 100644 modules/silauth/src/models/FailedLoginIpAddressBase.php create mode 100644 modules/silauth/src/models/FailedLoginUsername.php create mode 100644 modules/silauth/src/models/FailedLoginUsernameBase.php create mode 100644 modules/silauth/src/rebuildbasemodels.sh create mode 100644 modules/silauth/src/saml/User.php create mode 100644 modules/silauth/src/system/System.php create mode 100644 modules/silauth/src/tests/bootstrap.php create mode 100644 modules/silauth/src/tests/fakes/FakeFailedIdBroker.php create mode 100644 modules/silauth/src/tests/fakes/FakeIdBroker.php create mode 100644 modules/silauth/src/tests/fakes/FakeInvalidIdBroker.php create mode 100644 modules/silauth/src/tests/fakes/FakeSuccessfulIdBroker.php create mode 100644 modules/silauth/src/tests/phpunit.xml create mode 100644 modules/silauth/src/tests/unit/auth/AuthenticatorTest.php create mode 100644 modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php create mode 100644 modules/silauth/src/tests/unit/captcha/DummySuccessfulCaptcha.php create mode 100644 modules/silauth/src/tests/unit/config/ConfigManagerTest.php create mode 100644 modules/silauth/src/tests/unit/csrf/CsrfProtectorTest.php create mode 100644 modules/silauth/src/tests/unit/csrf/FakeSession.php create mode 100644 modules/silauth/src/tests/unit/http/DummyRequest.php create mode 100644 modules/silauth/src/tests/unit/http/RequestTest.php create mode 100644 modules/silauth/src/tests/unit/models/FailedLoginIpAddressTest.php create mode 100644 modules/silauth/src/tests/unit/models/FailedLoginUsernameTest.php create mode 100644 modules/silauth/src/tests/unit/text/TextTest.php create mode 100644 modules/silauth/src/tests/unit/time/UtcTimeTest.php create mode 100644 modules/silauth/src/tests/unit/time/WaitTimeTest.php create mode 100644 modules/silauth/src/text/Text.php create mode 100644 modules/silauth/src/time/UtcTime.php create mode 100644 modules/silauth/src/time/WaitTime.php create mode 100644 modules/silauth/src/traits/LoggerAwareTrait.php create mode 100755 modules/silauth/src/yii create mode 100644 modules/silauth/www/loginuserpass.php create mode 100644 modules/silauth/www/status.php diff --git a/README.md b/README.md index 8523efe3..4de8f9e7 100644 --- a/README.md +++ b/README.md @@ -246,3 +246,83 @@ https://stackoverflow.com/q/46566014/3813891 This is adapted from the `silinternational/simplesamlphp-module-mfa` module, which itself is adapted from other modules. Thanks to all those who contributed to that work. + +### SilAuth SimpleSAMLphp module + +SimpleSAMLphp auth module implementing custom business logic: + +- authentication +- rate limiting +- status endpoint + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/silinternational/simplesamlphp-module-silauth/develop/LICENSE) + +#### Database Migrations +To create another database migration file, run the following (replacing +`YourMigrationName` with whatever you want the migration to be named, using +CamelCase): + + make migration NAME=YourMigrationName + +#### Rate Limiting +SilAuth will rate limit failed logins by username and by every untrusted IP +address from a login attempt. + +##### tl;dr ("the short version") +If there have been more than 10 failed logins for a given username (or IP +address) within the past hour, a captcha will be included in the webpage. The +user may or may not have to directly interact with the captcha, though. + +If there have been more than 50 failed logins for that username (or IP address) +within the past hour, logins for that username (or IP address) will be blocked +for up to an hour. + +##### Details +For each login attempt, if it has too many failed logins within the last hour +(aka. recent failed logins) for the given username OR for any single untrusted +IP address associated with the current request, it will do one of the following: + +- If there are fewer than `Authenticator::REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN` + recent failures: process the request normally. +- If there are at least that many, but fewer than + `Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN`: require the user to pass a + captcha. +- If there are more than that: block that login attempt for `(recent failures + above the limit)^2` seconds after the most recent failed login, with a + minimum of 3 (so blocking for 9 seconds). +- Note: the blocking time is capped at an hour, so if no more failures occur, + then the user will be unblocked in no more than an hour. + +See `features/login.feature` for descriptions of how various situations are +handled. That file not only contains human-readable scenarios, but those are +also actual tests that are run to ensure those descriptions are correct. + +##### Example 1 + +- If `BLOCK_AFTER_NTH_FAILED_LOGIN` is 50, and +- if `REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN` is 10, and +- if there have been 4 failed login attempts for `john_smith`, and +- there have been 10 failed login attempts from `11.22.33.44`, and +- there have been 3 failed login attempts from `192.168.1.2`, and +- someone tries to login as `john_smith` from `192.168.1.2` and their request + goes through a proxy at `11.22.33.44`, then +- they will have to pass a captcha, but they will not yet be blocked. + +##### Example 2 + +- However, if all of the above is true, but +- there have now been 55 failed login attempts from `11.22.33.44`, then +- any request involving that IP address will be blocked for 25 seconds after + the most recent of those failed logins. + +#### Excluding trusted IP addresses from IP address based rate limiting +Since this application enforces rate limits based on the number of recent +failed login attempts by both username and IP address, and since it looks at +both the REMOTE_ADDR and the X-Forwarded-For header for IP addresses, you will +want to list any IP addresses that should NOT be rate limited (such as your +load balancer) in the TRUSTED_IP_ADDRESSES environment variable (see +`local.env.dist`). + +#### Status Check +To check the status of the website, you can access this URL: +`https://(your domain name)/module.php/silauth/status.php` diff --git a/actions-services.yml b/actions-services.yml index 32bfe9d2..817ccb20 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -1,5 +1,15 @@ version: '3' services: + + # the db container is used by the silauth module + db: + image: mariadb:10 + environment: + MYSQL_ROOT_PASSWORD: r00tp@ss! + MYSQL_DATABASE: silauth + MYSQL_USER: silauth + MYSQL_PASSWORD: silauth + app: build: . depends_on: @@ -176,3 +186,45 @@ services: - SAML20_IDP_ENABLE=false - ADMIN_PROTECT_INDEX_PAGE=false - THEME_USE=default + + # the broker and brokerDb containers are used by the silauth module + broker: + image: silintl/idp-id-broker:latest + ports: + - "80" + depends_on: + - brokerDb + environment: + IDP_NAME: "idp" + MYSQL_HOST: "brokerDb" + MYSQL_DATABASE: "broker" + MYSQL_USER: "user" + MYSQL_PASSWORD: "pass" + EMAIL_SERVICE_accessToken: "dummy" + EMAIL_SERVICE_assertValidIp: "false" + EMAIL_SERVICE_baseUrl: "dummy" + EMAILER_CLASS: Sil\SilIdBroker\Behat\Context\fakes\FakeEmailer + HELP_CENTER_URL: "https://example.org/help" + PASSWORD_FORGOT_URL: "https://example.org/forgot" + PASSWORD_PROFILE_URL: "https://example.org/profile" + SUPPORT_EMAIL: "support@example.org" + EMAIL_SIGNATURE: "one red pill, please" + API_ACCESS_KEYS: "test-cli-abc123" + APP_ENV: "prod" + MFA_TOTP_apiBaseUrl: not_needed_here + MFA_TOTP_apiKey: not_needed_here + MFA_TOTP_apiSecret: not_needed_here + MFA_WEBAUTHN_apiBaseUrl: not_needed_here + MFA_WEBAUTHN_apiKey: not_needed_here + MFA_WEBAUTHN_apiSecret: not_needed_here + command: "bash -c 'whenavail brokerDb 3306 60 ./yii migrate --interactive=0 && ./run.sh'" + + brokerDb: + image: mariadb:10 + ports: + - "3306" + environment: + MYSQL_ROOT_PASSWORD: "r00tp@ss!" + MYSQL_DATABASE: "broker" + MYSQL_USER: "user" + MYSQL_PASSWORD: "pass" diff --git a/behat.yml b/behat.yml index a5ecc748..e05a2b46 100644 --- a/behat.yml +++ b/behat.yml @@ -6,6 +6,9 @@ default: expiry_features: paths: [ '%paths.base%//features//expirychecker.feature' ] contexts: [ 'ExpiryContext' ] + login_features: + paths: [ '%paths.base%//features//login.feature' ] + contexts: [ 'LoginContext' ] material_features: paths: [ '%paths.base%//features//material.feature' ] contexts: [ 'FeatureContext' ] @@ -15,3 +18,6 @@ default: profilereview_features: paths: [ '%paths.base%//features//profilereview.feature' ] contexts: [ 'ProfileReviewContext' ] + status_features: + paths: [ '%paths.base%//features//status.feature' ] + contexts: [ 'StatusContext' ] diff --git a/docker-compose.yml b/docker-compose.yml index 178786d3..5edf404a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,15 @@ services: MYSQL_USER: silauth MYSQL_PASSWORD: silauth + dbadmin: + image: phpmyadmin/phpmyadmin + ports: + - "8080:80" + environment: + PMA_HOST: db + PMA_USER: silauth + PMA_PASSWORD: silauth + test: build: . depends_on: @@ -297,6 +306,44 @@ services: - ADMIN_PROTECT_INDEX_PAGE=false - THEME_USE=default + # the broker and brokerDb containers are used by the silauth module + broker: + image: silintl/idp-id-broker:develop + ports: + - "80" + depends_on: + - brokerDb + env_file: + - ./local.broker.env + environment: + IDP_NAME: "idp" + MYSQL_HOST: "brokerDb" + MYSQL_DATABASE: "broker" + MYSQL_USER: "user" + MYSQL_PASSWORD: "pass" + EMAIL_SERVICE_accessToken: "dummy" + EMAIL_SERVICE_assertValidIp: "false" + EMAIL_SERVICE_baseUrl: "dummy" + EMAILER_CLASS: Sil\SilIdBroker\Behat\Context\fakes\FakeEmailer + HELP_CENTER_URL: "https://example.org/help" + PASSWORD_FORGOT_URL: "https://example.org/forgot" + PASSWORD_PROFILE_URL: "https://example.org/profile" + SUPPORT_EMAIL: "support@example.org" + EMAIL_SIGNATURE: "one red pill, please" + API_ACCESS_KEYS: "test-cli-abc123" + APP_ENV: "dev" + command: ["bash", "-c", "whenavail brokerDb 3306 60 ./yii migrate --interactive=0 && ./run.sh"] + + brokerDb: + image: mariadb:10 + ports: + - "3306" + environment: + MYSQL_ROOT_PASSWORD: "r00tp@ss!" + MYSQL_DATABASE: "broker" + MYSQL_USER: "user" + MYSQL_PASSWORD: "pass" + networks: default: driver: bridge diff --git a/features/bootstrap/LoginContext.php b/features/bootstrap/LoginContext.php new file mode 100644 index 00000000..2e38ebd9 --- /dev/null +++ b/features/bootstrap/LoginContext.php @@ -0,0 +1,643 @@ + ['db' => [ + 'dsn' => sprintf( + 'mysql:host=%s;dbname=%s', + Env::get('MYSQL_HOST'), + Env::get('MYSQL_DATABASE') + ), + 'username' => Env::get('MYSQL_USER'), + 'password' => Env::get('MYSQL_PASSWORD'), + ]]]); + + $this->logger = new Psr3EchoLogger(); + + $this->captcha = new Captcha(); + $this->idBroker = new IdBroker( + 'http://fake.example.com/api/', + 'FakeAccessToken', + $this->logger, + 'fake.example.com', + [], + false + ); + $this->request = new Request(); + + $this->resetDatabase(); + } + + protected function addXFailedLoginUsernames(int $number, $username) + { + Assert::notEmpty($username); + + for ($i = 0; $i < $number; $i++) { + $newRecord = new FailedLoginUsername(['username' => $username]); + Assert::true($newRecord->save()); + } + + Assert::count( + FailedLoginUsername::getFailedLoginsFor($username), + $number + ); + } + + protected function login() + { + $this->authenticator = new Authenticator( + $this->username, + $this->password, + $this->request, + $this->captcha, + $this->idBroker, + $this->logger + ); + } + + protected function loginXTimes($numberOfTimes) + { + for ($i = 0; $i < $numberOfTimes; $i++) { + $this->login(); + } + } + + protected function resetDatabase() + { + FailedLoginIpAddress::deleteAll(); + FailedLoginUsername::deleteAll(); + } + + /** + * @Given I provide a username + */ + public function iProvideAUsername() + { + $this->username = 'a username'; + } + + /** + * @Given I provide a password + */ + public function iProvideAPassword() + { + $this->password = 'a password'; + } + + /** + * @When I try to log in + */ + public function iTryToLogIn() + { + $this->login(); + } + + /** + * @Then I should not be allowed through + */ + public function iShouldNotBeAllowedThrough() + { + Assert::false( + $this->authenticator->isAuthenticated() + ); + $authenticator = $this->authenticator; + Assert::throws( + function() use ($authenticator) { + $authenticator->getUserAttributes(); + }, + \Exception::class, + 'The call to getUserAttributes() should have thrown an exception.' + ); + } + + /** + * @Given I do not provide a username + */ + public function iDoNotProvideAUsername() + { + $this->username = ''; + } + + /** + * @Then I should see an error message with :text in it + */ + public function iShouldSeeAnErrorMessageWithInIt($text) + { + $authError = $this->authenticator->getAuthError(); + Assert::notEmpty($authError); + Assert::contains((string)$authError, $text); + } + + /** + * @Given I do not provide a password + */ + public function iDoNotProvideAPassword() + { + $this->password = ''; + } + + /** + * @Given I fail the captcha + */ + public function iFailTheCaptcha() + { + $this->captcha = new DummyFailedCaptcha(); + } + + /** + * @Then I should see a generic invalid-login error message + */ + public function iShouldSeeAGenericInvalidLoginErrorMessage() + { + $authError = $this->authenticator->getAuthError(); + Assert::notEmpty($authError); + Assert::contains((string)$authError, 'invalid_login'); + } + + /** + * @Given I provide a username of :username + */ + public function iProvideAUsernameOf($username) + { + $this->username = $username; + } + + /** + * @Then I should see an error message telling me to wait + */ + public function iShouldSeeAnErrorMessageTellingMeToWait() + { + $authError = $this->authenticator->getAuthError(); + Assert::notEmpty($authError); + Assert::contains((string)$authError, 'rate_limit'); + } + + /** + * @Given I provide an incorrect password + */ + public function iProvideAnIncorrectPassword() + { + $this->password = 'dummy incorrect password'; + $this->idBroker = new FakeFailedIdBroker('fake', 'fake', $this->logger); + } + + /** + * @Given that username will be rate limited after one more failed attempt + */ + public function thatUsernameWillBeRateLimitedAfterOneMoreFailedAttempt() + { + FailedLoginUsername::resetFailedLoginsBy($this->username); + + $this->addXFailedLoginUsernames( + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN - 1, + $this->username + ); + } + + /** + * @Given I (then) provide the correct password for that username + */ + public function iProvideTheCorrectPasswordForThatUsername() + { + Assert::notEmpty($this->username); + $this->password = 'dummy correct password'; + $this->idBroker = new FakeSuccessfulIdBroker('fake', 'fake', $this->logger); + } + + /** + * @Then I should not see an error message + */ + public function iShouldNotSeeAnErrorMessage() + { + $authError = $this->authenticator->getAuthError(); + Assert::isEmpty( + $authError, + "Unexpected error: \n- " . $authError + ); + } + + /** + * @Then I should be allowed through + */ + public function iShouldBeAllowedThrough() + { + Assert::true( + $this->authenticator->isAuthenticated() + ); + $userInfo = $this->authenticator->getUserAttributes(); + Assert::notEmpty($userInfo); + } + + /** + * @When I try to log in enough times to trigger the rate limit + */ + public function iTryToLogInEnoughTimesToTriggerTheRateLimit() + { + $this->loginXTimes( + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN + ); + } + + /** + * @Given that username has :number more recent failed logins than the limit + */ + public function thatUsernameHasMoreRecentFailedLoginsThanTheLimit($number) + { + Assert::true(is_numeric($number)); + + FailedLoginUsername::resetFailedLoginsBy($this->username); + + $this->addXFailedLoginUsernames( + $number + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN, + $this->username + ); + } + + /** + * @Then I should see an error message with :text1 and :text2 in it + */ + public function iShouldSeeAnErrorMessageWithAndInIt($text1, $text2) + { + $authError = $this->authenticator->getAuthError(); + Assert::notEmpty($authError); + $authErrorString = (string)$authError; + Assert::contains($authErrorString, $text1); + Assert::contains($authErrorString, $text2); + } + + /** + * @Given that username has enough failed logins to require a captcha + */ + public function thatUsernameHasEnoughFailedLoginsToRequireACaptcha() + { + FailedLoginUsername::resetFailedLoginsBy($this->username); + + $this->addXFailedLoginUsernames( + Authenticator::REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN, + $this->username + ); + } + + /** + * @Given that username has no recent failed login attempts + */ + public function thatUsernameHasNoRecentFailedLoginAttempts() + { + Assert::notEmpty($this->username); + FailedLoginUsername::resetFailedLoginsBy($this->username); + Assert::eq( + 0, + FailedLoginUsername::countRecentFailedLoginsFor($this->username) + ); + } + + /** + * @Then that username should be blocked for awhile + */ + public function thatUsernameShouldBeBlockedForAwhile() + { + Assert::notEmpty($this->username); + Assert::true( + FailedLoginUsername::isRateLimitBlocking($this->username) + ); + } + + /** + * @Given my request comes from IP address :ipAddress + */ + public function myRequestComesFromIpAddress($ipAddress) + { + if ( ! $this->request instanceof DummyRequest) { + $this->request = new DummyRequest(); + } + + $this->request->setDummyIpAddress($ipAddress); + } + + /** + * @Then that IP address should be blocked for awhile + */ + public function thatIpAddressShouldBeBlockedForAwhile() + { + $ipAddresses = $this->request->getUntrustedIpAddresses(); + Assert::count($ipAddresses, 1); + $ipAddress = $ipAddresses[0]; + + Assert::true( + FailedLoginIpAddress::isRateLimitBlocking($ipAddress) + ); + } + + /** + * @Then that username's failed login attempts should be at :number + */ + public function thatUsernameSFailedLoginAttemptsShouldBeAt($number) + { + Assert::notEmpty($this->username); + Assert::true(is_numeric($number)); + Assert::count( + FailedLoginUsername::getFailedLoginsFor($this->username), + (int)$number + ); + } + + /** + * @Given that username does not have enough failed logins to require a captcha + */ + public function thatUsernameDoesNotHaveEnoughFailedLoginsToRequireACaptcha() + { + Assert::notEmpty($this->username); + FailedLoginUsername::deleteAll(); + Assert::isEmpty(FailedLoginUsername::getFailedLoginsFor($this->username)); + } + + /** + * @Given my IP address has enough failed logins to require a captcha + */ + public function myIpAddressHasEnoughFailedLoginsToRequireACaptcha() + { + $ipAddress = $this->request->getMostLikelyIpAddress(); + Assert::notNull($ipAddress, 'No IP address was provided.'); + FailedLoginIpAddress::deleteAll(); + Assert::isEmpty(FailedLoginIpAddress::getFailedLoginsFor($ipAddress)); + + $desiredCount = Authenticator::REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN; + + for ($i = 0; $i < $desiredCount; $i++) { + $failedLoginIpAddress = new FailedLoginIpAddress([ + 'ip_address' => $ipAddress, + ]); + Assert::true($failedLoginIpAddress->save()); + } + + Assert::eq( + Authenticator::REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN, + FailedLoginIpAddress::countRecentFailedLoginsFor($ipAddress) + ); + } + + /** + * @Given that username has enough failed logins to be blocked by the rate limit + */ + public function thatUsernameHasEnoughFailedLoginsToBeBlockedByTheRateLimit() + { + FailedLoginUsername::resetFailedLoginsBy($this->username); + + $this->addXFailedLoginUsernames( + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN, + $this->username + ); + } + + /** + * @Given that IP address has triggered the rate limit + */ + public function thatIpAddressHasTriggeredTheRateLimit() + { + $ipAddresses = $this->request->getUntrustedIpAddresses(); + Assert::count($ipAddresses, 1); + $ipAddress = $ipAddresses[0]; + + FailedLoginIpAddress::deleteAll(); + Assert::isEmpty(FailedLoginIpAddress::getFailedLoginsFor($ipAddress)); + + $desiredCount = Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN; + + for ($i = 0; $i < $desiredCount; $i++) { + $failedLoginIpAddress = new FailedLoginIpAddress([ + 'ip_address' => $ipAddress, + ]); + Assert::true($failedLoginIpAddress->save()); + } + + Assert::true( + FailedLoginIpAddress::isRateLimitBlocking($ipAddress) + ); + } + + /** + * @Given /^I pass (the|any) captchas?$/ + */ + public function iPassTheCaptcha() + { + $this->captcha = new DummySuccessfulCaptcha(); + } + + /** + * @Given that username has :number more non-recent failed logins than the limit + */ + public function thatUsernameHasMoreNonRecentFailedLoginsThanTheLimit($number) + { + Assert::notEmpty($this->username); + Assert::true(is_numeric($number)); + + $desiredNumber = $number + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN; + + $numTotalFailures = count(FailedLoginUsername::getFailedLoginsFor($this->username)); + $numRecentFailures = FailedLoginUsername::countRecentFailedLoginsFor($this->username); + $numNonRecentFailures = $numTotalFailures - $numRecentFailures; + + for ($i = $numNonRecentFailures; $i < $desiredNumber; $i++) { + $failedLoginUsername = new FailedLoginUsername([ + 'username' => $this->username, + + // NOTE: Use some time (UTC) longer ago than we consider "recent". + 'occurred_at_utc' => new UtcTime('-1 month'), + ]); + // NOTE: Don't validate, as that would overwrite the datetime field. + Assert::true($failedLoginUsername->save(false)); + } + + $numTotalFailuresPost = count(FailedLoginUsername::getFailedLoginsFor($this->username)); + $numRecentFailuresPost = FailedLoginUsername::countRecentFailedLoginsFor($this->username); + $numNonRecentFailuresPost = $numTotalFailuresPost - $numRecentFailuresPost; + + Assert::eq($desiredNumber, $numNonRecentFailuresPost); + } + + /** + * @Then I should not have to pass a captcha test for that user + */ + public function iShouldNotHaveToPassACaptchaTestForThatUser() + { + Assert::notEmpty($this->username); + Assert::false( + FailedLoginUsername::isCaptchaRequiredFor($this->username) + ); + } + + /** + * @Given :ipAddress is a trusted IP address + */ + public function isATrustedIpAddress($ipAddress) + { + $this->request->trustIpAddress($ipAddress); + } + + /** + * @Then the IP address :ipAddress should not have any failed login attempts + */ + public function theIpAddressShouldNotHaveAnyFailedLoginAttempts($ipAddress) + { + Assert::true(Request::isValidIpAddress($ipAddress)); + Assert::isEmpty(FailedLoginIpAddress::getFailedLoginsFor($ipAddress)); + } + + /** + * @Given the ID Broker is returning invalid responses + */ + public function theIdBrokerIsReturningInvalidResponses() + { + $this->idBroker = new FakeInvalidIdBroker('fake', 'fake', $this->logger); + } + + /** + * @Then I should see a generic try-later error message + */ + public function iShouldSeeAGenericTryLaterErrorMessage() + { + $authError = $this->authenticator->getAuthError(); + Assert::notEmpty($authError); + Assert::contains((string)$authError, 'later'); + } + + /** + * @Given :ipAddressRange is a trusted IP address range + */ + public function isATrustedIpAddressRange($ipAddressRange) + { + $this->request->trustIpAddressRange($ipAddressRange); + } + + /** + * @Then the IP address :ipAddress should have a failed login attempt + */ + public function theIpAddressShouldHaveAFailedLoginAttempt($ipAddress) + { + Assert::notEmpty($ipAddress); + Assert::count( + FailedLoginIpAddress::getFailedLoginsFor($ipAddress), + 1 + ); + } + + /** + * @Given :numSeconds seconds ago that username had :numFailuresBeyondLimit more failed logins than the limit + */ + public function secondsAgoThatUsernameHadMoreFailedLoginsThanTheLimit( + $numSeconds, + $numFailuresBeyondLimit + ) { + Assert::notEmpty($this->username); + Assert::true(is_numeric($numSeconds)); + Assert::true(is_numeric($numFailuresBeyondLimit)); + + FailedLoginUsername::resetFailedLoginsBy($this->username); + + $numDesiredFailuresTotal = $numFailuresBeyondLimit + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN; + + for ($i = 0; $i < $numDesiredFailuresTotal; $i++) { + $failedLoginUsername = new FailedLoginUsername([ + 'username' => $this->username, + 'occurred_at_utc' => new UtcTime(sprintf( + '-%s seconds', + $numSeconds + )), + ]); + // NOTE: Don't validate, as that would overwrite the datetime field. + Assert::true($failedLoginUsername->save(false)); + } + + $numTotalFailuresPost = count(FailedLoginUsername::getFailedLoginsFor($this->username)); + + Assert::eq($numDesiredFailuresTotal, $numTotalFailuresPost); + } + + /** + * @Given :numSeconds seconds ago the IP address :ipAddress had :numFailuresBeyondLimit more failed logins than the limit + */ + public function secondsAgoTheIpAddressHadMoreFailedLoginsThanTheLimit( + $numSeconds, + $ipAddress, + $numFailuresBeyondLimit + ) { + Assert::notEmpty($ipAddress); + Assert::true(is_numeric($numSeconds)); + Assert::true(is_numeric($numFailuresBeyondLimit)); + + FailedLoginIpAddress::resetFailedLoginsBy([$ipAddress]); + + $numDesiredFailuresTotal = $numFailuresBeyondLimit + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN; + + for ($i = 0; $i < $numDesiredFailuresTotal; $i++) { + $failedLoginIpAddress = new FailedLoginIpAddress([ + 'ip_address' => $ipAddress, + 'occurred_at_utc' => new UtcTime(sprintf( + '-%s seconds', + $numSeconds + )), + ]); + // NOTE: Don't validate, as that would overwrite the datetime field. + Assert::true($failedLoginIpAddress->save(false)); + } + + $numTotalFailuresPost = count(FailedLoginIpAddress::getFailedLoginsFor($ipAddress)); + + Assert::eq($numDesiredFailuresTotal, $numTotalFailuresPost); + } +} diff --git a/features/bootstrap/StatusContext.php b/features/bootstrap/StatusContext.php new file mode 100644 index 00000000..b6e9b689 --- /dev/null +++ b/features/bootstrap/StatusContext.php @@ -0,0 +1,56 @@ +get('http://testweb/module.php/silauth/status.php'); + $this->responseCode = $response->getStatusCode(); + $this->responseText = $response->getBody()->getContents(); + } + + /** + * @Then I should get back a(n) :responseText with an HTTP status code of :statusCode + */ + public function iShouldGetBackAWithAnHttpStatusCodeOf($responseText, $statusCode) + { + Assert::same($this->responseText, $responseText); + Assert::eq($this->responseCode, $statusCode); + } + + /** + * @When I request the initial login page of this module + */ + public function iRequestTheInitialLoginPageOfThisModule() + { + $client = new Client([ + 'cookies' => true, + 'http_errors' => false, + ]); + $response = $client->get('http://testweb/module.php/core/authenticate.php?as=silauth'); + $this->responseCode = $response->getStatusCode(); + } + + /** + * @Then I should get back an HTTP status code of :statusCode + */ + public function iShouldGetBackAnHttpStatusCodeOf($statusCode) + { + Assert::eq($this->responseCode, $statusCode); + } +} diff --git a/features/login.feature b/features/login.feature new file mode 100644 index 00000000..77e9a2d8 --- /dev/null +++ b/features/login.feature @@ -0,0 +1,207 @@ +Feature: User login + In order to log in + As a user + I need to provide an acceptable username and password + + Rules + - Username and password are both required. + - The user is only allowed through if all of the necessary checks pass. + + Scenario: Failing to provide a username + Given I provide a password + But I do not provide a username + When I try to log in + Then I should see an error message with "username" in it + And I should not be allowed through + + Scenario: Failing to provide a password + Given I provide a username + But I do not provide a password + When I try to log in + Then I should see an error message with "password" in it + And I should not be allowed through + + Scenario: Enough failed logins to require a captcha for a username + Given I provide a username + And I provide the correct password for that username + But that username has enough failed logins to require a captcha + And I fail the captcha + When I try to log in + Then I should see a generic invalid-login error message + And I should not be allowed through + + Scenario: Enough failed logins to require a captcha for an IP address + Given my request comes from IP address "11.22.33.44" + And I provide a username + And I provide the correct password for that username + And that username does not have enough failed logins to require a captcha + But my IP address has enough failed logins to require a captcha + And I fail the captcha + When I try to log in + Then I should see a generic invalid-login error message + And I should not be allowed through + + Scenario: Trying to log in with a rate-limited username + Given I provide a username + And I provide a password + But that username has enough failed logins to be blocked by the rate limit + When I try to log in + Then I should see an error message telling me to wait + And that username should be blocked for awhile + And I should not be allowed through + + Scenario: Trying to log in with a rate-limited IP address + Given I provide a username + And I provide a password + And my request comes from IP address "11.22.33.44" + And that IP address has triggered the rate limit + When I try to log in + Then I should see an error message telling me to wait + And that IP address should be blocked for awhile + And I should not be allowed through + + Scenario: Providing unacceptable credentials + Given I provide a username + And that username has no recent failed login attempts + But I provide an incorrect password + When I try to log in + Then I should see a generic invalid-login error message + And I should not be allowed through + + Scenario: Providing unacceptable credentials that trigger a rate limit + Given I provide a username + And that username will be rate limited after one more failed attempt + And I pass the captcha + But I provide an incorrect password + When I try to log in + Then I should see an error message telling me to wait + And that username should be blocked for awhile + And I should not be allowed through + + Scenario: Providing a correct username-password combination + Given I provide a username + And I provide the correct password for that username + And that username has no recent failed login attempts + When I try to log in + Then I should not see an error message + And I should be allowed through + + Scenario: Providing too many incorrect username-password combinations + Given I provide a username + And I provide an incorrect password + And I pass any captchas + When I try to log in enough times to trigger the rate limit + Then I should see an error message telling me to wait + And that username should be blocked for awhile + And I should not be allowed through + + Scenario: Providing correct credentials after one failed login attempt + Given I provide a username + And I provide an incorrect password + And I try to log in + But I then provide the correct password for that username + When I try to log in + Then I should not see an error message + And I should be allowed through + And that username's failed login attempts should be at 0 + + Scenario: Being told about how long to wait (due to rate limiting bad logins) + Given I provide a username + And I provide the correct password for that username + But that username has 5 more recent failed logins than the limit + When I try to log in + Then I should see an error message with "30" and "seconds" in it + And that username should be blocked for awhile + And I should not be allowed through + + Scenario: Logging in right before a username rate limit has expired + Given I provide a username + And I provide the correct password for that username + And 24 seconds ago that username had 5 more failed logins than the limit + And I pass any captchas + When I try to log in + Then I should see an error message with "5" and "seconds" in it + And I should not be allowed through + + Scenario: Logging in right after a username rate limit has expired + Given I provide a username + And I provide the correct password for that username + And 26 seconds ago that username had 5 more failed logins than the limit + And I pass any captchas + When I try to log in + Then I should not see an error message + And I should be allowed through + + Scenario: Logging in right before an IP address rate limit has expired + Given my request comes from IP address "11.22.33.44" + And I provide a username + And I provide the correct password for that username + And 24 seconds ago the IP address "11.22.33.44" had 5 more failed logins than the limit + And I pass any captchas + When I try to log in + Then I should see an error message with "5" and "seconds" in it + And I should not be allowed through + + Scenario: Logging in right after an IP address rate limit has expired + Given my request comes from IP address "11.22.33.44" + And I provide a username + And I provide the correct password for that username + And 26 seconds ago the IP address "11.22.33.44" had 5 more failed logins than the limit + And I pass any captchas + When I try to log in + Then I should not see an error message + And I should be allowed through + + Scenario: No failed logins (and thus no captcha requirement) + Given I provide a username + When that username has no recent failed login attempts + Then I should not have to pass a captcha test for that user + + Scenario: Not restricting requests from a trusted IPv4 address + Given I provide a username + And I provide an incorrect password + And my request comes from IP address "11.22.33.44" + But "11.22.33.44" is a trusted IP address + When I try to log in + Then I should see a generic invalid-login error message + And I should not be allowed through + But the IP address "11.22.33.44" should not have any failed login attempts + + Scenario: Not restricting requests from a trusted IPv6 address + Given I provide a username + And I provide an incorrect password + And my request comes from IP address "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + But "2001:0db8:85a3::8a2e:0370:7334" is a trusted IP address + When I try to log in + Then I should see a generic invalid-login error message + And I should not be allowed through + But the IP address "2001:0db8:85a3:0000:0000:8a2e:0370:7334" should not have any failed login attempts + + Scenario: Not restricting requests from an IP address in a trusted range + Given I provide a username + And I provide an incorrect password + And my request comes from IP address "11.22.33.44" + But "11.22.33.0/24" is a trusted IP address range + When I try to log in + Then I should see a generic invalid-login error message + And I should not be allowed through + But the IP address "11.22.33.44" should not have any failed login attempts + + Scenario: Restricting requests from an IP address NOT in a trusted range + Given I provide a username + And I provide an incorrect password + And my request comes from IP address "11.22.33.44" + And "11.22.55.0/24" is a trusted IP address range + When I try to log in + Then I should see a generic invalid-login error message + And I should not be allowed through + And the IP address "11.22.33.44" should have a failed login attempt + + Scenario: Invalid response from the ID Broker + Given I provide a username + And I provide the correct password for that username + And that username has no recent failed login attempts + But the ID Broker is returning invalid responses + When I try to log in + Then I should see a generic try-later error message + And I should not be allowed through diff --git a/features/status.feature b/features/status.feature new file mode 100644 index 00000000..660997e2 --- /dev/null +++ b/features/status.feature @@ -0,0 +1,9 @@ +Feature: Status check + + Scenario: Good status check + When I check the status of this module + Then I should get back an "OK" with an HTTP status code of "200" + + Scenario: Request initial login page + When I request the initial login page of this module + Then I should get back an HTTP status code of "200" diff --git a/local.broker.env.dist b/local.broker.env.dist new file mode 100755 index 00000000..59eca0a9 --- /dev/null +++ b/local.broker.env.dist @@ -0,0 +1,18 @@ + +### Required ENV vars ### +MFA_TOTP_apiBaseUrl= +MFA_TOTP_apiKey= +MFA_TOTP_apiSecret= + +MFA_WEBAUTHN_apiBaseUrl= +MFA_WEBAUTHN_apiKey= +MFA_WEBAUTHN_apiSecret= +MFA_WEBAUTHN_appId= +MFA_WEBAUTHN_rpDisplayName= +MFA_WEBAUTHN_rpId= + +# Comma separated array of origins allowed as relying parties (with scheme, without port or path) +RP_ORIGINS= + +### Optional ENV vars ### +COMPOSER_AUTH="{\"github-oauth\": {\"github.com\": \"YOUR TOKEN HERE\"}}" diff --git a/local.env.dist b/local.env.dist index 23c9cdbf..9f8fd47b 100644 --- a/local.env.dist +++ b/local.env.dist @@ -80,3 +80,15 @@ MFA_LEARN_MORE_URL= # The URL to send a user to for setting up their MFA. # Example: https://pw.example.com/#/2sv/intro MFA_SETUP_URL= + +# silauth config + +# List any IP addresses and/or IP address ranges (CIDR) that should NOT be +# rate-limited but which might be included in REMOTE_ADDR or the X-Forwarded-For +# header (such as your application's load balancer). +# Example: TRUSTED_IP_ADDRESSES=11.22.33.44,11.22.55.0/24 +TRUSTED_IP_ADDRESSES= + +# See "https://developers.google.com/recaptcha/docs/faq" for test key/secret. +RECAPTCHA_SITE_KEY= +RECAPTCHA_SECRET= diff --git a/modules/silauth/dictionaries/error.definition.json b/modules/silauth/dictionaries/error.definition.json new file mode 100644 index 00000000..52225585 --- /dev/null +++ b/modules/silauth/dictionaries/error.definition.json @@ -0,0 +1,50 @@ +{ + "generic_try_later": { + "en": "Hmm... something went wrong. Please try again later.", + "es": "", + "fr": "", + "ko": "" + }, + "username_required": { + "en": "Please provide a username.", + "es": "", + "fr": "", + "ko": "" + }, + "password_required": { + "en": "Please provide a password.", + "es": "", + "fr": "", + "ko": "" + }, + "invalid_login": { + "en": "There was a problem with that username or password (or that account is disabled). Please try again or contact your organization's help desk.", + "es": "", + "fr": "", + "ko": "" + }, + "need_to_set_acct_password": { + "en": "You need to set your password to finish setting up your account. Please use the forgot password link below.", + "es": "", + "fr": "", + "ko": "" + }, + "rate_limit_seconds": { + "en": "There have been too many failed logins for this account. Please wait about {number} seconds, then try again.", + "es": "", + "fr": "", + "ko": "" + }, + "rate_limit_1_minute": { + "en": "There have been too many failed logins for this account. Please wait a minute, then try again.", + "es": "", + "fr": "", + "ko": "" + }, + "rate_limit_minutes": { + "en": "There have been too many failed logins for this account. Please wait about {number} minutes, then try again.", + "es": "", + "fr": "", + "ko": "" + } +} diff --git a/modules/silauth/lib/Auth/Source/SilAuth.php b/modules/silauth/lib/Auth/Source/SilAuth.php new file mode 100644 index 00000000..9b9b76d6 --- /dev/null +++ b/modules/silauth/lib/Auth/Source/SilAuth.php @@ -0,0 +1,160 @@ +authConfig = ConfigManager::getConfigFor('auth', $config); + $this->idBrokerConfig = ConfigManager::getConfigFor('idBroker', $config); + $this->mysqlConfig = ConfigManager::getConfigFor('mysql', $config); + $this->recaptchaConfig = ConfigManager::getConfigFor('recaptcha', $config); + $this->templateData = ConfigManager::getConfigFor('templateData', $config); + + ConfigManager::initializeYii2WebApp(['components' => ['db' => [ + 'dsn' => sprintf( + 'mysql:host=%s;dbname=%s', + $this->mysqlConfig['host'], + $this->mysqlConfig['database'] + ), + 'username' => $this->mysqlConfig['user'], + 'password' => $this->mysqlConfig['password'], + ]]]); + } + + /** + * Initialize login. + * + * This function saves the information about the login, and redirects to a + * login page. + * + * @param array &$state Information about the current authentication. + */ + public function authenticate(&$state) + { + assert('is_array($state)'); + + /* + * Save the identifier of this authentication source, so that we can + * retrieve it later. This allows us to call the login()-function on + * the current object. + */ + $state[self::AUTHID] = $this->authId; + + $state['templateData'] = $this->templateData; + + /* Save the $state-array, so that we can restore it after a redirect. */ + $id = State::saveState($state, self::STAGEID); + + /* + * Redirect to the login form. We include the identifier of the saved + * state array as a parameter to the login form. + */ + $url = Module::getModuleURL('silauth/loginuserpass.php'); + $params = array('AuthState' => $id); + HTTP::redirectTrustedURL($url, $params); + + /* The previous function never returns, so this code is never executed. */ + assert('FALSE'); + } + + protected function getTrustedIpAddresses() + { + $trustedIpAddresses = []; + $ipAddressesString = $this->authConfig['trustedIpAddresses'] ?? ''; + $stringPieces = explode(',', $ipAddressesString); + foreach ($stringPieces as $stringPiece) { + if (! empty($stringPiece)) { + $trustedIpAddresses[] = $stringPiece; + } + } + return $trustedIpAddresses; + } + + protected function login($username, $password) + { + $logger = new Psr3StdOutLogger(); + $captcha = new Captcha($this->recaptchaConfig['secret'] ?? null); + $idBroker = new IdBroker( + $this->idBrokerConfig['baseUri'] ?? null, + $this->idBrokerConfig['accessToken'] ?? null, + $logger, + $this->idBrokerConfig['idpDomainName'], + $this->idBrokerConfig['trustedIpRanges'] ?? [], + $this->idBrokerConfig['assertValidIp'] ?? true + ); + $request = new Request($this->getTrustedIpAddresses()); + $userAgent = Request::getUserAgent() ?: '(unknown)'; + $authenticator = new Authenticator( + $username, + $password, + $request, + $captcha, + $idBroker, + $logger + ); + + if (! $authenticator->isAuthenticated()) { + $authError = $authenticator->getAuthError(); + $logger->warning(json_encode([ + 'event' => 'User/pass authentication result: failure', + 'username' => $username, + 'errorCode' => $authError->getCode(), + 'errorMessageParams' => $authError->getMessageParams(), + 'ipAddresses' => join(',', $request->getIpAddresses()), + 'userAgent' => $userAgent, + ])); + throw new Error([ + 'WRONGUSERPASS', + $authError->getFullSspErrorTag(), + $authError->getMessageParams() + ]); + } + + $logger->warning(json_encode([ + 'event' => 'User/pass authentication result: success', + 'username' => $username, + 'ipAddresses' => join(',', $request->getIpAddresses()), + 'userAgent' => $userAgent, + ])); + + return $authenticator->getUserAttributes(); + } +} diff --git a/modules/silauth/src/auth/AuthError.php b/modules/silauth/src/auth/AuthError.php new file mode 100644 index 00000000..b08b1656 --- /dev/null +++ b/modules/silauth/src/auth/AuthError.php @@ -0,0 +1,72 @@ +code = $code; + $this->messageParams = $messageParams; + } + + public function __toString() + { + return var_export([ + 'code' => $this->code, + 'messageParams' => $this->messageParams, + ], true); + } + + /** + * Get the error code, which will be one of the AuthError::CODE_* constants. + * + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * Get the error string that should be passed to simpleSAMLphp's translate + * function for this AuthError. It will correspond to an entry in the + * appropriate dictionary file provided by this module. + * + * @return string Example: '{silauth:error:generic_try_later}' + */ + public function getFullSspErrorTag() + { + return sprintf( + '{%s:%s}', + 'silauth:error', + $this->getCode() + ); + } + + public function getMessageParams() + { + return $this->messageParams; + } +} diff --git a/modules/silauth/src/auth/Authenticator.php b/modules/silauth/src/auth/Authenticator.php new file mode 100644 index 00000000..3d085c59 --- /dev/null +++ b/modules/silauth/src/auth/Authenticator.php @@ -0,0 +1,363 @@ +logger = $logger; + + /** @todo Check CSRF here, too, if feasible. */ + + if (empty($username)) { + $this->setErrorUsernameRequired(); + return; + } + + if (empty($password)) { + $this->setErrorPasswordRequired(); + return; + } + + $ipAddresses = $request->getUntrustedIpAddresses(); + + if ($this->isBlockedByRateLimit($username, $ipAddresses)) { + $logger->warning(json_encode([ + 'event' => 'Preventing login attempt due to existing rate limit', + 'username' => $username, + 'ipAddresses' => join(',', $ipAddresses), + ])); + $this->setErrorBlockedByRateLimit( + $this->getWaitTimeUntilUnblocked($username, $ipAddresses) + ); + return; + } + + if (self::isCaptchaRequired($username, $ipAddresses)) { + $logger->warning(json_encode([ + 'event' => 'Requiring captcha', + 'username' => $username, + 'ipAddresses' => join(',', $ipAddresses), + ])); + if ( ! $captcha->isValidIn($request)) { + $logger->warning(json_encode([ + 'event' => 'Invalid/missing captcha', + 'username' => $username, + 'ipAddresses' => join(',', $ipAddresses), + ])); + $this->setErrorInvalidLogin(); + return; + } + } + + try { + $authenticatedUser = $idBroker->getAuthenticatedUser( + $username, + $password + ); + } catch (\Exception $e) { + $logger->critical(json_encode([ + 'event' => 'Problem communicating with ID Broker', + 'errorCode' => $e->getCode(), + 'errorMessage' => $e->getMessage(), + 'username' => $username, + 'ipAddresses' => join(',', $ipAddresses), + ])); + $this->setErrorGenericTryLater(); + return; + } + + if ($authenticatedUser === null) { + $this->recordFailedLoginBy($username, $ipAddresses); + + if ($this->isBlockedByRateLimit($username, $ipAddresses)) { + $logger->warning(json_encode([ + 'event' => 'Activating rate-limit block', + 'username' => $username, + 'ipAddresses' => join(',', $ipAddresses), + ])); + $this->setErrorBlockedByRateLimit( + $this->getWaitTimeUntilUnblocked($username, $ipAddresses) + ); + } else { + $this->setErrorInvalidLogin(); + } + return; + } + + // NOTE: If we reach this point, the user successfully authenticated. + + $this->resetFailedLoginsBy($username, $ipAddresses); + + $this->setUserAttributes($authenticatedUser); + } + + /** + * Calculate how many seconds of delay should be required for the given + * number of recent failed login attempts. + * + * @param int $numRecentFailures The number of recent failed login attempts. + * @return int The number of seconds to delay before allowing another such + * login attempt. + */ + public static function calculateSecondsToDelay($numRecentFailures) + { + if ( ! self::isEnoughFailedLoginsToBlock($numRecentFailures)) { + return 0; + } + + $limit = self::BLOCK_AFTER_NTH_FAILED_LOGIN; + $numFailuresPastLimit = $numRecentFailures - $limit; + $numberToUse = max($numFailuresPastLimit, 3); + + return min( + $numberToUse * $numberToUse, + self::MAX_SECONDS_TO_BLOCK + ); + } + + /** + * Get the error information (if any). + * + * @return AuthError|null + */ + public function getAuthError() + { + return $this->authError; + } + + /** + * Get the number of seconds to continue blocking, based on the given number + * of recent failures and the given date/time of the most recent failed + * login attempt. + * + * @param int $numRecentFailures The number of recent failed login attempts. + * @param string|null $mostRecentFailureAt A date/time string for when the + * most recent failed login attempt occurred. If null (meaning there + * have been no recent failures), then zero (0) will be returned. + * @return int The number of seconds + * @throws Exception If an invalid (but non-null) date/time string is given + * for `$mostRecentFailureAt`. + */ + public static function getSecondsUntilUnblocked( + int $numRecentFailures, + $mostRecentFailureAt + ) { + if ($mostRecentFailureAt === null) { + return 0; + } + + $totalSecondsToBlock = self::calculateSecondsToDelay( + $numRecentFailures + ); + + $secondsSinceLastFailure = UtcTime::getSecondsSinceDateTime( + $mostRecentFailureAt + ); + + return UtcTime::getRemainingSeconds( + $totalSecondsToBlock, + $secondsSinceLastFailure + ); + } + + /** + * Get the attributes about the authenticated user. + * + * @return array The user attributes. Example:
+     *     [
+     *         // ...
+     *         'mail' => ['someone@example.com'],
+     *         // ...
+     *     ]
+     *     
+ * @throws \Exception + */ + public function getUserAttributes() + { + if ($this->userAttributes === null) { + throw new \Exception( + "You cannot get the user's attributes until you have authenticated the user.", + 1482270373 + ); + } + + return $this->userAttributes; + } + + /** + * Get a (user friendly) wait time representing how long the user must wait + * until they will no longer be blocked by a rate limit (regardless of + * whether it is their username and/or IP address that is blocked). + * + * NOTE: This will always return a WaitTime, even if the given username and + * IP addresses aren't blocked (in which case the shortest available + * WaitTime will be returned, such as a 5-second wait time). + * + * @param string $username The username in question. + * @param array $ipAddresses The list of relevant IP addresses (related to + * this request). + * @return WaitTime + */ + protected function getWaitTimeUntilUnblocked($username, array $ipAddresses) + { + $durationsInSeconds = [ + FailedLoginUsername::getSecondsUntilUnblocked($username), + ]; + + foreach ($ipAddresses as $ipAddress) { + $durationsInSeconds[] = FailedLoginIpAddress::getSecondsUntilUnblocked($ipAddress); + } + + return WaitTime::getLongestWaitTime($durationsInSeconds); + } + + protected function hasError() + { + return ($this->authError !== null); + } + + /** + * Check whether authentication was successful. If not, call + * getErrorMessage() and/or getErrorCode() to find out why not. + * + * @return bool + */ + public function isAuthenticated() + { + return ( ! $this->hasError()); + } + + protected function isBlockedByRateLimit($username, array $ipAddresses) + { + return FailedLoginUsername::isRateLimitBlocking($username) || + FailedLoginIpAddress::isRateLimitBlockingAnyOfThese($ipAddresses); + } + + public static function isCaptchaRequired($username, array $ipAddresses) + { + return FailedLoginUsername::isCaptchaRequiredFor($username) || + FailedLoginIpAddress::isCaptchaRequiredForAnyOfThese($ipAddresses); + } + + public static function isEnoughFailedLoginsToBlock($numFailedLogins) + { + return ($numFailedLogins >= self::BLOCK_AFTER_NTH_FAILED_LOGIN); + } + + public static function isEnoughFailedLoginsToRequireCaptcha($numFailedLogins) + { + return ($numFailedLogins >= self::REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN); + } + + protected function recordFailedLoginBy($username, array $ipAddresses) + { + FailedLoginUsername::recordFailedLoginBy($username, $this->logger); + FailedLoginIpAddress::recordFailedLoginBy($ipAddresses, $this->logger); + } + + protected function resetFailedLoginsBy($username, array $ipAddresses) + { + FailedLoginUsername::resetFailedLoginsBy($username); + FailedLoginIpAddress::resetFailedLoginsBy($ipAddresses); + } + + protected function setError($code, $messageParams = []) + { + $this->authError = new AuthError($code, $messageParams); + } + + /** + * @param WaitTime $waitTime + */ + protected function setErrorBlockedByRateLimit($waitTime) + { + $unit = $waitTime->getUnit(); + $number = $waitTime->getFriendlyNumber(); + + if ($unit === WaitTime::UNIT_SECOND) { + $errorCode = AuthError::CODE_RATE_LIMIT_SECONDS; + } else { // = minute + if ($number === 1) { + $errorCode = AuthError::CODE_RATE_LIMIT_1_MINUTE; + } else { + $errorCode = AuthError::CODE_RATE_LIMIT_MINUTES; + } + } + + $this->setError($errorCode, ['{number}' => $number]); + } + + protected function setErrorGenericTryLater() + { + $this->setError(AuthError::CODE_GENERIC_TRY_LATER); + } + + protected function setErrorInvalidLogin() + { + $this->setError(AuthError::CODE_INVALID_LOGIN); + } + + protected function setErrorNeedToSetAcctPassword() + { + $this->setError(AuthError::CODE_NEED_TO_SET_ACCT_PASSWORD); + } + + protected function setErrorPasswordRequired() + { + $this->setError(AuthError::CODE_PASSWORD_REQUIRED); + } + + protected function setErrorUsernameRequired() + { + $this->setError(AuthError::CODE_USERNAME_REQUIRED); + } + + protected function setUserAttributes($attributes) + { + $this->userAttributes = $attributes; + } +} diff --git a/modules/silauth/src/auth/IdBroker.php b/modules/silauth/src/auth/IdBroker.php new file mode 100644 index 00000000..ff1509f9 --- /dev/null +++ b/modules/silauth/src/auth/IdBroker.php @@ -0,0 +1,109 @@ +logger = $logger; + $this->idpDomainName = $idpDomainName; + $this->client = new IdBrokerClient($baseUri, $accessToken, [ + 'http_client_options' => [ + 'timeout' => 10, + ], + IdBrokerClient::TRUSTED_IPS_CONFIG => $trustedIpRanges, + IdBrokerClient::ASSERT_VALID_BROKER_IP_CONFIG => $assertValidIp, + ]); + } + + /** + * Attempt to authenticate with the given username and password, returning + * the attributes for that user if the credentials were acceptable (or null + * if they were not acceptable, since there is no authenticated user in that + * situation). If an unexpected response is received, an exception will be + * thrown. + * + * NOTE: The attributes names used (if any) in the response will be SAML + * field names, not ID Broker field names. + * + * @param string $username The username. + * @param string $password The password. + * @return array|null The user's attributes (if successful), otherwise null. + * @throws \Exception + */ + public function getAuthenticatedUser(string $username, string $password) + { + $rpOrigin = 'https://' . $this->idpDomainName; + $userInfo = $this->client->authenticate($username, $password, $rpOrigin); + + if ($userInfo === null) { + return null; + } + + $pwExpDate = $userInfo['password']['expires_on'] ?? null; + if ($pwExpDate !== null) { + $schacExpiryDate = gmdate('YmdHis\Z', strtotime($pwExpDate)); + } + + return SamlUser::convertToSamlFieldNames( + $userInfo['employee_id'], + $userInfo['first_name'], + $userInfo['last_name'], + $userInfo['username'], + $userInfo['email'], + $userInfo['uuid'], + $this->idpDomainName, + $schacExpiryDate ?? null, + $userInfo['mfa'], + $userInfo['method'], + $userInfo['manager_email'] ?? null, + $userInfo['profile_review'] ?? 'no', + $userInfo['member'] ?? [] + ); + } + + /** + * Ping the /site/status URL. If the ID Broker's status is fine, the + * response string is returned. If not, an exception is thrown. + * + * @return string "OK" + * @throws Exception + */ + public function getSiteStatus() + { + return $this->client->getSiteStatus(); + } +} diff --git a/modules/silauth/src/behaviors/CreatedAtUtcBehavior.php b/modules/silauth/src/behaviors/CreatedAtUtcBehavior.php new file mode 100644 index 00000000..b9ceb1bb --- /dev/null +++ b/modules/silauth/src/behaviors/CreatedAtUtcBehavior.php @@ -0,0 +1,22 @@ +value === null) { + return UtcTime::format(); + } + return parent::getValue($event); + } +} diff --git a/modules/silauth/src/captcha/Captcha.php b/modules/silauth/src/captcha/Captcha.php new file mode 100644 index 00000000..4f3f05e4 --- /dev/null +++ b/modules/silauth/src/captcha/Captcha.php @@ -0,0 +1,29 @@ +secret = $secret; + } + + public function isValidIn(Request $request) + { + if (empty($this->secret)) { + throw new \RuntimeException('No captcha secret available.', 1487342411); + } + + $captchaResponse = $request->getCaptchaResponse(); + $ipAddress = $request->getMostLikelyIpAddress(); + + $recaptcha = new \ReCaptcha\ReCaptcha($this->secret); + $rcResponse = $recaptcha->verify($captchaResponse, $ipAddress); + + return $rcResponse->isSuccess(); + } +} diff --git a/modules/silauth/src/config/ConfigManager.php b/modules/silauth/src/config/ConfigManager.php new file mode 100644 index 00000000..d8f3f040 --- /dev/null +++ b/modules/silauth/src/config/ConfigManager.php @@ -0,0 +1,110 @@ + $value) { + if (Text::startsWith($key, $categoryPrefix)) { + $subKey = self::removeCategory($key); + $categoryConfig[$subKey] = $value; + } + } + return $categoryConfig; + } + + /** + * Get the Yii2 config, merging in the given custom config data. + * + * @param array $customConfig + * @return array + */ + public static function getMergedYii2Config($customConfig) + { + $defaultConfig = require __DIR__ . '/yii2-config.php'; + return array_replace_recursive( + $defaultConfig, + $customConfig + ); + } + + private static function initializeYiiClass() + { + if ( ! class_exists('Yii')) { + require_once __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php'; + } + } + + public static function getYii2ConsoleApp($customConfig) + { + self::initializeYiiClass(); + $mergedYii2Config = self::getMergedYii2Config($customConfig); + return new \yii\console\Application($mergedYii2Config); + } + + public static function initializeYii2WebApp($customConfig = []) + { + self::initializeYiiClass(); + + /* Initialize the Yii web application. Note that we do NOT call run() + * here, since we don't want Yii to handle the HTTP request. We just + * want the Yii classes available for use (including database + * models). */ + $app = new \yii\web\Application(self::getMergedYii2Config($customConfig)); + + /* + * Initialize the Yii logger. It doesn't want to initialize itself for some reason. + */ + $app->log->getLogger(); + } + + public static function removeCategory($key) + { + if ($key === null) { + return null; + } + $pieces = explode(self::SEPARATOR, $key, 2); + return end($pieces); + } +} diff --git a/modules/silauth/src/config/ssp-config.php b/modules/silauth/src/config/ssp-config.php new file mode 100644 index 00000000..63951801 --- /dev/null +++ b/modules/silauth/src/config/ssp-config.php @@ -0,0 +1,21 @@ + Env::get('TRUSTED_IP_ADDRESSES'), + 'idBroker.accessToken' => Env::get('ID_BROKER_ACCESS_TOKEN'), + 'idBroker.assertValidIp' => Env::get('ID_BROKER_ASSERT_VALID_IP'), + 'idBroker.baseUri' => Env::get('ID_BROKER_BASE_URI'), + 'idBroker.trustedIpRanges' => Env::getArray('ID_BROKER_TRUSTED_IP_RANGES'), + 'idBroker.idpDomainName' => Env::requireEnv('IDP_DOMAIN_NAME'), + 'mysql.host' => Env::get('MYSQL_HOST'), + 'mysql.database' => Env::get('MYSQL_DATABASE'), + 'mysql.user' => Env::get('MYSQL_USER'), + 'mysql.password' => Env::get('MYSQL_PASSWORD'), + 'recaptcha.siteKey' => Env::get('RECAPTCHA_SITE_KEY'), + 'recaptcha.secret' => Env::get('RECAPTCHA_SECRET'), + 'templateData.profileUrl' => Env::get('PROFILE_URL'), + 'templateData.helpCenterUrl' => Env::get('HELP_CENTER_URL'), +]; diff --git a/modules/silauth/src/config/yii2-config.php b/modules/silauth/src/config/yii2-config.php new file mode 100644 index 00000000..b40c03ee --- /dev/null +++ b/modules/silauth/src/config/yii2-config.php @@ -0,0 +1,72 @@ + __DIR__ . '/../', + 'id' => 'SilAuth', + 'aliases' => [ + '@Sil/SilAuth' => __DIR__ . '/..', + ], + 'bootstrap' => [ + 'gii', + ], + 'components' => [ + 'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => null, + 'username' => null, + 'password' => null, + ], + 'log' => [ + 'targets' => [ + [ + 'class' => JsonStreamTarget::class, + 'url' => 'php://stdout', + 'levels' => ['info'], + 'logVars' => [], + 'categories' => ['application'], + 'prefix' => function ($message) { + $prefixData = [ + 'message' => $message, + 'env' => YII_ENV, + ]; + return Json::encode($prefixData); + }, + 'exportInterval' => 1, + ], + [ + 'class' => JsonStreamTarget::class, + 'url' => 'php://stderr', + 'levels' => ['error', 'warning'], + 'logVars' => [], + 'prefix' => function ($message) { + $prefixData = [ + 'message' => $message, + 'env' => YII_ENV, + ]; + return Json::encode($prefixData); + }, + 'exportInterval' => 1, + ], + ], + ], + ], + 'controllerMap' => [ + 'migrate' => [ + 'class' => 'yii\console\controllers\MigrateController', + 'migrationNamespaces' => [ + 'Sil\\SilAuth\\migrations\\', + ], + + // Disable non-namespaced migrations. + 'migrationPath' => null, + ], + ], + 'modules' => [ + 'gii' => [ + 'class' => 'yii\gii\Module', + ], + ], +]; diff --git a/modules/silauth/src/csrf/CsrfProtector.php b/modules/silauth/src/csrf/CsrfProtector.php new file mode 100644 index 00000000..110575e4 --- /dev/null +++ b/modules/silauth/src/csrf/CsrfProtector.php @@ -0,0 +1,81 @@ +session = $session; + } + + public function changeMasterToken() + { + $newMasterToken = $this->generateToken(); + $this->setTokenInSession($newMasterToken); + } + + protected function generateToken() + { + return bin2hex(random_bytes(32)); + } + + /** + * Get the CSRF protection token from the session. If not found, a new one + * will be generated and stored in the session. + * + * @return string The master (aka. authoritative) CSRF token. + */ + public function getMasterToken() + { + $masterToken = $this->getTokenFromSession(); + if (empty($masterToken)) { + $masterToken = $this->generateToken(); + $this->setTokenInSession($masterToken); + } + return $masterToken; + } + + protected function getTokenFromSession() + { + return $this->session->getData( + $this->csrfTokenDataType, + $this->csrfSessionKey + ); + } + + /** + * Check the given CSRF token to see if it was correct. + * + * @param string $submittedToken The CSRF protection token provided by the + * HTTP request. + * @return bool + */ + public function isTokenCorrect($submittedToken) + { + return hash_equals($this->getMasterToken(), $submittedToken); + } + + protected function setTokenInSession($masterToken) + { + $this->session->setData( + $this->csrfTokenDataType, + $this->csrfSessionKey, + $masterToken + ); + } +} diff --git a/modules/silauth/src/http/Request.php b/modules/silauth/src/http/Request.php new file mode 100644 index 00000000..13c11e1e --- /dev/null +++ b/modules/silauth/src/http/Request.php @@ -0,0 +1,212 @@ +isValidIpAddress($ipAddress)) { + $this->trustIpAddress($ipAddress); + } else { + $this->trustIpAddressRange($ipAddress); + } + } + } + + public function getCaptchaResponse() + { + return self::sanitizeInputString(INPUT_POST, 'g-recaptcha-response'); + } + + /** + * Get the list of IP addresses from the current HTTP request. They will be + * in order such that the last IP address in the list belongs to the device + * that most recently handled the request (probably our load balancer). The + * IP address first in the list is both (A) more likely to be the user's + * actual IP address and (B) most likely to be forged. + * + * @return string[] A list of IP addresses. + */ + public function getIpAddresses() + { + $ipAddresses = []; + + // First add the X-Forwarded-For IP addresses. + $xForwardedFor = self::sanitizeInputString( + INPUT_SERVER, + 'HTTP_X_FORWARDED_FOR' + ); + foreach (explode(',', $xForwardedFor) as $xffIpAddress) { + $trimmedIpAddress = trim($xffIpAddress); + if (self::isValidIpAddress($trimmedIpAddress)) { + $ipAddresses[] = $trimmedIpAddress; + } + } + + /* Finally, add the REMOTE_ADDR server value, since it belongs to the + * device that directly passed this request to our application. */ + $remoteAddr = self::sanitizeInputString(INPUT_SERVER, 'REMOTE_ADDR'); + if (self::isValidIpAddress($remoteAddr)) { + $ipAddresses[] = $remoteAddr; + } + + return $ipAddresses; + } + + /** + * Get the IP address that this request most likely originated from. + * + * @return string|null An IP address, or null if none was available. + */ + public function getMostLikelyIpAddress() + { + $untrustedIpAddresses = $this->getUntrustedIpAddresses(); + + /* Given the way X-Forwarded-For (and other?) headers work, the last + * entry in the list was the IP address of the system closest to our + * application. Once we filter out trusted IP addresses (such as that of + * our load balancer, etc.), the last remaining IP address in the list + * is probably the one we care about: + * + * "Since it is easy to forge an X-Forwarded-For field the given + * information should be used with care. The last IP address is always + * the IP address that connects to the last proxy, which means it is + * the most reliable source of information." + * - https://en.wikipedia.org/wiki/X-Forwarded-For + */ + $userIpAddress = end($untrustedIpAddresses); + + /* Make sure we actually ended up with an IP address (not FALSE, which + * `last()` would return if there were no entries). */ + return self::isValidIpAddress($userIpAddress) ? $userIpAddress : null; + } + + /** + * Retrieve input data (see `filter_input(...)` for details) as a string but + * DO NOT sanitize it. If it is a string, it will be returned as is. If it + * is not a string, an empty string will be returned, so that the return + * type will always be a string. + * + * @param int $inputType Example: INPUT_POST + * @param string $variableName Example: 'username' + * @return string + */ + public static function getRawInputString(int $inputType, string $variableName) + { + $input = filter_input($inputType, $variableName); + return is_string($input) ? $input : ''; + } + + public function getUntrustedIpAddresses() + { + $untrustedIpAddresses = []; + foreach ($this->getIpAddresses() as $ipAddress) { + if ( ! $this->isTrustedIpAddress($ipAddress)) { + $untrustedIpAddresses[] = $ipAddress; + } + } + return $untrustedIpAddresses; + } + + /** + * Get the User-Agent string. + * + * @return string The UA string, or an empty string if not found. + */ + public static function getUserAgent() + { + return self::sanitizeInputString(INPUT_SERVER, 'HTTP_USER_AGENT'); + } + + /** + * Determine whether the given IP address is trusted (either specifically or + * because it is in a trusted range). + * + * @param string $ipAddress The IP address in question. + * @return bool + */ + public function isTrustedIpAddress($ipAddress) + { + foreach ($this->trustedIpAddresses as $trustedIp) { + if ($trustedIp->numeric() === IP::create($ipAddress)->numeric()) { + return true; + } + } + + foreach ($this->trustedIpAddressRanges as $trustedIpBlock) { + if ($trustedIpBlock->containsIP($ipAddress)) { + return true; + } + } + + return false; + } + + /** + * Check that a given string is a valid IP address (IPv4 or IPv6). + * + * @param string $ipAddress The IP address in question. + * @return bool + */ + public static function isValidIpAddress($ipAddress) + { + $flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6; + return (filter_var($ipAddress, FILTER_VALIDATE_IP, $flags) !== false); + } + + /** + * Retrieve input data (see `filter_input(...)` for details) and sanitize + * it (see Text::sanitizeString). + * + * @param int $inputType Example: INPUT_POST + * @param string $variableName Example: 'username' + * @return string + */ + public static function sanitizeInputString(int $inputType, string $variableName) + { + return Text::sanitizeString(filter_input($inputType, $variableName)); + } + + public function trustIpAddress($ipAddress) + { + if ( ! self::isValidIpAddress($ipAddress)) { + throw new \InvalidArgumentException(sprintf( + '%s is not a valid IP address.', + var_export($ipAddress, true) + )); + } + $this->trustedIpAddresses[] = IP::create($ipAddress); + } + + public function trustIpAddressRange($ipAddressRangeString) + { + $ipBlock = IPBlock::create($ipAddressRangeString); + $this->trustedIpAddressRanges[] = $ipBlock; + } +} diff --git a/modules/silauth/src/migrations/M161213135750CreateInitialTables.php b/modules/silauth/src/migrations/M161213135750CreateInitialTables.php new file mode 100644 index 00000000..9ba70c7d --- /dev/null +++ b/modules/silauth/src/migrations/M161213135750CreateInitialTables.php @@ -0,0 +1,62 @@ +createTable('{{user}}', [ + 'id' => 'pk', + 'uuid' => 'string NOT NULL', + 'employee_id' => 'string NOT NULL', + 'first_name' => 'string NOT NULL', + 'last_name' => 'string NOT NULL', + 'username' => 'string NOT NULL', + 'email' => 'string NOT NULL', + 'password_hash' => 'string NULL', + 'active' => "enum('Yes','No') NOT NULL DEFAULT 'Yes'", + 'locked' => "enum('No','Yes') NOT NULL DEFAULT 'No'", + 'login_attempts' => 'integer NOT NULL DEFAULT 0', + 'block_until' => 'datetime NULL', + 'last_updated' => 'datetime NOT NULL', + ], 'ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci'); + $this->createIndex('uq_user_uuid', '{{user}}', 'uuid', true); + $this->createIndex('uq_user_employee_id', '{{user}}', 'employee_id', true); + $this->createIndex('uq_user_username', '{{user}}', 'username', true); + $this->createIndex('uq_user_email', '{{user}}', 'email', true); + + $this->createTable('{{previous_password}}', [ + 'id' => 'pk', + 'user_id' => 'integer NOT NULL', + 'password_hash' => 'string NOT NULL', + 'created' => 'datetime NOT NULL', + ], 'ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci'); + $this->addForeignKey( + 'fk_prev_pw_user_user_id', + '{{previous_password}}', + 'user_id', + '{{user}}', + 'id', + 'CASCADE', + 'CASCADE' + ); + } + + public function safeDown() + { + $this->dropForeignKey( + 'fk_prev_pw_user_user_id', + '{{previous_password}}' + ); + $this->dropTable('{{previous_password}}'); + + $this->dropIndex('uq_user_uuid', '{{user}}'); + $this->dropIndex('uq_user_employee_id', '{{user}}'); + $this->dropIndex('uq_user_username', '{{user}}'); + $this->dropIndex('uq_user_email', '{{user}}'); + $this->dropTable('{{user}}'); + } +} diff --git a/modules/silauth/src/migrations/M161213150831SwitchToUtcForDateTimes.php b/modules/silauth/src/migrations/M161213150831SwitchToUtcForDateTimes.php new file mode 100644 index 00000000..4b7f7d41 --- /dev/null +++ b/modules/silauth/src/migrations/M161213150831SwitchToUtcForDateTimes.php @@ -0,0 +1,22 @@ +renameColumn('{{user}}', 'block_until', 'block_until_utc'); + $this->renameColumn('{{user}}', 'last_updated', 'last_updated_utc'); + $this->renameColumn('{{previous_password}}', 'created', 'created_utc'); + } + + public function safeDown() + { + $this->renameColumn('{{previous_password}}', 'created_utc', 'created'); + $this->renameColumn('{{user}}', 'last_updated_utc', 'last_updated'); + $this->renameColumn('{{user}}', 'block_until_utc', 'block_until'); + } +} diff --git a/modules/silauth/src/migrations/M170214141109CreateFailedLoginsTable.php b/modules/silauth/src/migrations/M170214141109CreateFailedLoginsTable.php new file mode 100644 index 00000000..94ac1e01 --- /dev/null +++ b/modules/silauth/src/migrations/M170214141109CreateFailedLoginsTable.php @@ -0,0 +1,39 @@ +createTable('{{failed_logins}}', [ + 'id' => 'pk', + 'username' => 'string NOT NULL', + 'ip_address' => 'varchar(45) NOT NULL', + 'occurred_at_utc' => 'datetime NOT NULL', + ], 'ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci'); + $this->createIndex( + 'idx_failed_logins_username', + '{{failed_logins}}', + 'username', + false + ); + $this->createIndex( + 'idx_failed_logins_ip_address', + '{{failed_logins}}', + 'ip_address', + false + ); + } + + public function safeDown() + { + $this->dropIndex('idx_failed_logins_ip_address', '{{failed_logins}}'); + $this->dropIndex('idx_failed_logins_username', '{{failed_logins}}'); + $this->dropTable('{{failed_logins}}'); + } +} diff --git a/modules/silauth/src/migrations/M170214145629RemoveOldTables.php b/modules/silauth/src/migrations/M170214145629RemoveOldTables.php new file mode 100644 index 00000000..6b56d5e2 --- /dev/null +++ b/modules/silauth/src/migrations/M170214145629RemoveOldTables.php @@ -0,0 +1,30 @@ +dropForeignKey( + 'fk_prev_pw_user_user_id', + '{{previous_password}}' + ); + $this->dropTable('{{previous_password}}'); + + $this->dropIndex('uq_user_uuid', '{{user}}'); + $this->dropIndex('uq_user_employee_id', '{{user}}'); + $this->dropIndex('uq_user_username', '{{user}}'); + $this->dropIndex('uq_user_email', '{{user}}'); + $this->dropTable('{{user}}'); + } + + public function safeDown() + { + echo "M170214145629RemoveOldTables cannot be reverted.\n"; + + return false; + } +} diff --git a/modules/silauth/src/migrations/M170215141724SplitFailedLoginsTable.php b/modules/silauth/src/migrations/M170215141724SplitFailedLoginsTable.php new file mode 100644 index 00000000..7c6e401a --- /dev/null +++ b/modules/silauth/src/migrations/M170215141724SplitFailedLoginsTable.php @@ -0,0 +1,44 @@ +dropIndex('idx_failed_logins_ip_address', '{{failed_logins}}'); + $this->dropIndex('idx_failed_logins_username', '{{failed_logins}}'); + + // Split/update table and add new indexes. + $this->dropColumn('{{failed_logins}}', 'ip_address'); + $this->renameTable('{{failed_logins}}', '{{failed_login_username}}'); + $this->createIndex( + 'idx_failed_login_username_username', + '{{failed_login_username}}', + 'username', + false + ); + /* The max length needed to store an IP address is 45 characters. See + * http://stackoverflow.com/a/1076755/3813891 for details. */ + $this->createTable('{{failed_login_ip_address}}', [ + 'id' => 'pk', + 'ip_address' => 'varchar(45) NOT NULL', + 'occurred_at_utc' => 'datetime NOT NULL', + ], 'ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci'); + $this->createIndex( + 'idx_failed_login_ip_address_ip_address', + '{{failed_login_ip_address}}', + 'ip_address', + false + ); + } + + public function safeDown() + { + echo "M170215141724SplitFailedLoginsTable cannot be reverted.\n"; + return false; + } +} diff --git a/modules/silauth/src/models/FailedLoginIpAddress.php b/modules/silauth/src/models/FailedLoginIpAddress.php new file mode 100644 index 00000000..08030483 --- /dev/null +++ b/modules/silauth/src/models/FailedLoginIpAddress.php @@ -0,0 +1,159 @@ + Yii::t('app', 'IP Address'), + 'occurred_at_utc' => Yii::t('app', 'Occurred At (UTC)'), + ]); + } + + public function behaviors() + { + return [ + [ + 'class' => CreatedAtUtcBehavior::className(), + 'attributes' => [ + Model::EVENT_BEFORE_VALIDATE => 'occurred_at_utc', + ], + ], + ]; + } + + public static function countRecentFailedLoginsFor($ipAddress) + { + return self::find()->where([ + 'ip_address' => strtolower($ipAddress), + ])->andWhere([ + '>=', 'occurred_at_utc', UtcTime::format('-60 minutes') + ])->count(); + } + + public static function getFailedLoginsFor($ipAddress) + { + if ( ! Request::isValidIpAddress($ipAddress)) { + throw new \InvalidArgumentException(sprintf( + '%s is not a valid IP address.', + var_export($ipAddress, true) + )); + } + + return self::findAll(['ip_address' => strtolower($ipAddress)]); + } + + /** + * Get the most recent failed-login record for the given IP address, or null + * if none is found. + * + * @param string $ipAddress The IP address. + * @return FailedLoginIpAddress|null + */ + public static function getMostRecentFailedLoginFor($ipAddress) + { + return self::find()->where([ + 'ip_address' => strtolower($ipAddress), + ])->orderBy([ + 'occurred_at_utc' => SORT_DESC, + ])->one(); + } + + /** + * Get the number of seconds remaining until the specified IP address is + * no longer blocked by a rate-limit. Returns zero if it is not currently + * blocked. + * + * @param string $ipAddress The IP address in question + * @return int The number of seconds + */ + public static function getSecondsUntilUnblocked($ipAddress) + { + $failedLogin = self::getMostRecentFailedLoginFor($ipAddress); + + return Authenticator::getSecondsUntilUnblocked( + self::countRecentFailedLoginsFor($ipAddress), + $failedLogin->occurred_at_utc ?? null + ); + } + + public function init() + { + $this->initializeLogger(); + parent::init(); + } + + public static function isCaptchaRequiredFor($ipAddress) + { + return Authenticator::isEnoughFailedLoginsToRequireCaptcha( + self::countRecentFailedLoginsFor($ipAddress) + ); + } + + public static function isCaptchaRequiredForAnyOfThese(array $ipAddresses) + { + foreach ($ipAddresses as $ipAddress) { + if (self::isCaptchaRequiredFor($ipAddress)) { + return true; + } + } + return false; + } + + public static function isRateLimitBlocking($ipAddress) + { + $secondsUntilUnblocked = self::getSecondsUntilUnblocked($ipAddress); + return ($secondsUntilUnblocked > 0); + } + + public static function isRateLimitBlockingAnyOfThese($ipAddresses) + { + foreach ($ipAddresses as $ipAddress) { + if (self::isRateLimitBlocking($ipAddress)) { + return true; + } + } + return false; + } + + public static function recordFailedLoginBy( + array $ipAddresses, + LoggerInterface $logger + ) { + foreach ($ipAddresses as $ipAddress) { + $newRecord = new FailedLoginIpAddress(['ip_address' => strtolower($ipAddress)]); + + if ( ! $newRecord->save()) { + $logger->critical(json_encode([ + 'event' => 'Failed to update login attempts counter in ' + . 'database, so unable to rate limit that IP address.', + 'ipAddress' => $ipAddress, + 'errors' => $newRecord->getErrors(), + ])); + } + } + } + + public static function resetFailedLoginsBy(array $ipAddresses) + { + foreach ($ipAddresses as $ipAddress) { + self::deleteAll(['ip_address' => strtolower($ipAddress)]); + } + } +} diff --git a/modules/silauth/src/models/FailedLoginIpAddressBase.php b/modules/silauth/src/models/FailedLoginIpAddressBase.php new file mode 100644 index 00000000..2da66370 --- /dev/null +++ b/modules/silauth/src/models/FailedLoginIpAddressBase.php @@ -0,0 +1,47 @@ + 45], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('app', 'ID'), + 'ip_address' => Yii::t('app', 'Ip Address'), + 'occurred_at_utc' => Yii::t('app', 'Occurred At Utc'), + ]; + } +} diff --git a/modules/silauth/src/models/FailedLoginUsername.php b/modules/silauth/src/models/FailedLoginUsername.php new file mode 100644 index 00000000..61579520 --- /dev/null +++ b/modules/silauth/src/models/FailedLoginUsername.php @@ -0,0 +1,140 @@ + Yii::t('app', 'Occurred At (UTC)'), + ]); + } + + public function behaviors() + { + return [ + [ + 'class' => CreatedAtUtcBehavior::className(), + 'attributes' => [ + Model::EVENT_BEFORE_VALIDATE => 'occurred_at_utc', + ], + ], + ]; + } + + public static function countRecentFailedLoginsFor($username) + { + return self::find()->where([ + 'username' => strtolower($username), + ])->andWhere([ + '>=', 'occurred_at_utc', UtcTime::format('-60 minutes') + ])->count(); + } + + /** + * Find the records with the given username (if any). + * + * @param string $username The username. + * @return FailedLoginUsername[] An array of any matching records. + */ + public static function getFailedLoginsFor($username) + { + return self::findAll(['username' => strtolower($username)]); + } + + /** + * Get the most recent failed-login record for the given username, or null + * if none is found. + * + * @param string $username The username. + * @return FailedLoginUsername|null + */ + public static function getMostRecentFailedLoginFor($username) + { + return self::find()->where([ + 'username' => strtolower($username), + ])->orderBy([ + 'occurred_at_utc' => SORT_DESC, + ])->one(); + } + + /** + * Get the number of seconds remaining until the specified username is + * no longer blocked by a rate-limit. Returns zero if the user is not + * currently blocked. + * + * @param string $username The username in question + * @return int The number of seconds + */ + public static function getSecondsUntilUnblocked($username) + { + $failedLogin = self::getMostRecentFailedLoginFor($username); + + return Authenticator::getSecondsUntilUnblocked( + self::countRecentFailedLoginsFor($username), + $failedLogin->occurred_at_utc ?? null + ); + } + + public function init() + { + $this->initializeLogger(); + parent::init(); + } + + /** + * Find out whether a rate limit is blocking the specified username. + * + * @param string $username The username + * @return bool + */ + public static function isRateLimitBlocking($username) + { + $secondsUntilUnblocked = self::getSecondsUntilUnblocked($username); + return ($secondsUntilUnblocked > 0); + } + + public static function isCaptchaRequiredFor($username) + { + if (empty($username)) { + return false; + } + return Authenticator::isEnoughFailedLoginsToRequireCaptcha( + self::countRecentFailedLoginsFor($username) + ); + } + + public static function recordFailedLoginBy( + $username, + LoggerInterface $logger + ) { + $newRecord = new FailedLoginUsername(['username' => strtolower($username)]); + if ( ! $newRecord->save()) { + $logger->critical(json_encode([ + 'event' => 'Failed to update login attempts counter in ' + . 'database, so unable to rate limit that username.', + 'username' => $username, + 'errors' => $newRecord->getErrors(), + ])); + } + } + + public static function resetFailedLoginsBy($username) + { + self::deleteAll(['username' => strtolower($username)]); + } +} diff --git a/modules/silauth/src/models/FailedLoginUsernameBase.php b/modules/silauth/src/models/FailedLoginUsernameBase.php new file mode 100644 index 00000000..81cfb29f --- /dev/null +++ b/modules/silauth/src/models/FailedLoginUsernameBase.php @@ -0,0 +1,47 @@ + 255], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('app', 'ID'), + 'username' => Yii::t('app', 'Username'), + 'occurred_at_utc' => Yii::t('app', 'Occurred At Utc'), + ]; + } +} diff --git a/modules/silauth/src/rebuildbasemodels.sh b/modules/silauth/src/rebuildbasemodels.sh new file mode 100644 index 00000000..8e89563c --- /dev/null +++ b/modules/silauth/src/rebuildbasemodels.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +TABLES=(user previous_password) +SUFFIX="Base" + +declare -A models +models["failed_login_username"]="FailedLoginUsernameBase" +models["failed_login_ip_address"]="FailedLoginIpAddressBase" + +for i in "${!models[@]}"; do + CMD="/data/src/yii gii/model --tableName=$i --modelClass=${models[$i]} --generateRelations=all --enableI18N=1 --overwrite=1 --interactive=0 --ns=Sil\SilAuth\models" + echo $CMD + $CMD +done diff --git a/modules/silauth/src/saml/User.php b/modules/silauth/src/saml/User.php new file mode 100644 index 00000000..3bb525c6 --- /dev/null +++ b/modules/silauth/src/saml/User.php @@ -0,0 +1,61 @@ + [ + $username . '@' . $idpDomainName, + ], + + /** + * Don't use this misspelled version of eduPersonTargetedID. (Accidentally used in the past) + * @deprecated + * + * NOTE: Do NOT include eduPersonTargetedID. If you need it, use the + * core:TargetedID module (at the Hub, if using one) to generate an + * eduPersonTargetedID based on the eduPersonUniqueId attribute (below). + * + */ + 'eduPersonTargetID' => (array)$uuid, // Incorrect, deprecated + + /** + * Use this for a globally unique, non-human friendly, non-reassignable attribute + **/ + 'eduPersonUniqueId' => (array)$eduPersonUniqueId, + + 'sn' => (array)$lastName, + 'givenName' => (array)$firstName, + 'mail' => (array)$email, + 'employeeNumber' => (array)$employeeId, + 'cn' => (array)$username, + 'schacExpiryDate' => (array)$passwordExpirationDate, + 'mfa' => $mfa, + 'method' => $method, + 'uuid' => (array)$uuid, + 'manager_email' => [$managerEmail ?? ''], + 'profile_review' => [$profileReview], + 'member' => $member, + ]; + } +} diff --git a/modules/silauth/src/system/System.php b/modules/silauth/src/system/System.php new file mode 100644 index 00000000..9ba3b1fe --- /dev/null +++ b/modules/silauth/src/system/System.php @@ -0,0 +1,96 @@ +logger = $logger ?? new NullLogger(); + } + + protected function isDatabaseOkay() + { + try { + FailedLoginIpAddress::getMostRecentFailedLoginFor(''); + return true; + } catch (Throwable $t) { + $this->logError($t->getMessage()); + return false; + } + } + + protected function isRequiredConfigPresent() + { + $globalConfig = Configuration::getInstance(); + + /* + * NOTE: We require that SSP's baseurlpath configuration is set (and + * matches the corresponding RegExp) in order to prevent a + * security hole in \SimpleSAML\Utils\HTTP::getBaseURL() where the + * HTTP_HOST value (provided by the user's request) is used to + * build a trusted URL (see SimpleSaml\Module::authenticate()). + */ + $baseURL = $globalConfig->getString('baseurlpath', ''); + $avoidsSecurityHole = (preg_match('#^https?://.*/$#D', $baseURL) === 1); + return $avoidsSecurityHole; + } + + /** + * Check the status of the system, and throw an exception (that is safe to + * show to the public) if any serious error conditions are found. Log any + * problems, even if recoverable. + * + * @throws \Exception + */ + public function reportStatus() + { + if ( ! $this->isRequiredConfigPresent()) { + $this->reportError('Config problem', 1485984755); + } + + if ( ! $this->isDatabaseOkay()) { + $this->reportError('Database problem', 1485284407); + } + + echo 'OK'; + } + + /** + * Add an entry to our log about this error. + * + * @param string $message The error message. + */ + protected function logError($message) + { + $this->logger->error($message); + } + + /** + * Log this error and throw an exception (with an error message) for the + * calling code to handle. + * + * @param string $message The error message. + * @param int $code An error code. + * @throws \Exception + */ + protected function reportError($message, $code) + { + $this->logError($message); + throw new \Exception($message, $code); + } +} diff --git a/modules/silauth/src/tests/bootstrap.php b/modules/silauth/src/tests/bootstrap.php new file mode 100644 index 00000000..f6227546 --- /dev/null +++ b/modules/silauth/src/tests/bootstrap.php @@ -0,0 +1,14 @@ + ['db' => [ + 'dsn' => sprintf( + 'mysql:host=%s;dbname=%s', + Env::get('MYSQL_HOST'), + Env::get('MYSQL_DATABASE') + ), + 'username' => Env::get('MYSQL_USER'), + 'password' => Env::get('MYSQL_PASSWORD'), +]]]); diff --git a/modules/silauth/src/tests/fakes/FakeFailedIdBroker.php b/modules/silauth/src/tests/fakes/FakeFailedIdBroker.php new file mode 100644 index 00000000..776c4a4c --- /dev/null +++ b/modules/silauth/src/tests/fakes/FakeFailedIdBroker.php @@ -0,0 +1,21 @@ +logger->info('FAKE FAILURE: rejecting {username} and {password}.', [ + 'username' => var_export($username, true), + 'password' => var_export($password, true), + ]); + return parent::getAuthenticatedUser($username, $password); + } + + protected function getDesiredResponse() + { + return new Response(400); + } +} diff --git a/modules/silauth/src/tests/fakes/FakeIdBroker.php b/modules/silauth/src/tests/fakes/FakeIdBroker.php new file mode 100644 index 00000000..cc9125e5 --- /dev/null +++ b/modules/silauth/src/tests/fakes/FakeIdBroker.php @@ -0,0 +1,47 @@ +client = new IdBrokerClient($baseUri, $accessToken, [ + 'http_client_options' => [ + 'handler' => HandlerStack::create(new MockHandler( + + /* Set up several, since this may be called multiple times + * during a test: */ + array_fill( + 0, + Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN * 2, + $this->getDesiredResponse() + ) + )), + ], + IdBrokerClient::ASSERT_VALID_BROKER_IP_CONFIG => false, + ]); + } + + abstract protected function getDesiredResponse(); +} diff --git a/modules/silauth/src/tests/fakes/FakeInvalidIdBroker.php b/modules/silauth/src/tests/fakes/FakeInvalidIdBroker.php new file mode 100644 index 00000000..202cf4c4 --- /dev/null +++ b/modules/silauth/src/tests/fakes/FakeInvalidIdBroker.php @@ -0,0 +1,18 @@ +logger->info('FAKE ERROR: invalid/unexpected response.'); + return parent::getAuthenticatedUser($username, $password); + } + + protected function getDesiredResponse() + { + return new Response(404); + } +} diff --git a/modules/silauth/src/tests/fakes/FakeSuccessfulIdBroker.php b/modules/silauth/src/tests/fakes/FakeSuccessfulIdBroker.php new file mode 100644 index 00000000..1de6be97 --- /dev/null +++ b/modules/silauth/src/tests/fakes/FakeSuccessfulIdBroker.php @@ -0,0 +1,41 @@ +logger->info('FAKE SUCCESS: accepting {username} and {password}.', [ + 'username' => var_export($username, true), + 'password' => var_export($password, true), + ]); + return parent::getAuthenticatedUser($username, $password); + } + + protected function getDesiredResponse() + { + return new Response(200, [], json_encode([ + 'uuid' => '11111111-aaaa-1111-aaaa-111111111111', + 'employee_id' => '123', + 'first_name' => 'John', + 'last_name' => 'Smith', + 'display_name' => 'John Smith', + 'username' => 'john_smith', + 'email' => 'john_smith@example.com', + 'locked' => 'no', + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'no', + 'review' => 'no', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + 'review' => 'no', + 'options' => [], + ], + ])); + } +} diff --git a/modules/silauth/src/tests/phpunit.xml b/modules/silauth/src/tests/phpunit.xml new file mode 100644 index 00000000..052c4a07 --- /dev/null +++ b/modules/silauth/src/tests/phpunit.xml @@ -0,0 +1,4 @@ + + diff --git a/modules/silauth/src/tests/unit/auth/AuthenticatorTest.php b/modules/silauth/src/tests/unit/auth/AuthenticatorTest.php new file mode 100644 index 00000000..59cc8285 --- /dev/null +++ b/modules/silauth/src/tests/unit/auth/AuthenticatorTest.php @@ -0,0 +1,97 @@ + 0, 'expected' => 0], + ['failedLoginAttempts' => $blockAfterNth - 1, 'expected' => 0], + ['failedLoginAttempts' => $blockAfterNth, 'expected' => 9], + ['failedLoginAttempts' => $blockAfterNth + 1, 'expected' => 9], + ['failedLoginAttempts' => $blockAfterNth + 2, 'expected' => 9], + ['failedLoginAttempts' => $blockAfterNth + 3, 'expected' => 9], + ['failedLoginAttempts' => $blockAfterNth + 4, 'expected' => 16], + ['failedLoginAttempts' => $blockAfterNth + 5, 'expected' => 25], + ['failedLoginAttempts' => $blockAfterNth + 6, 'expected' => 36], + ['failedLoginAttempts' => $blockAfterNth + 10, 'expected' => 100], + ['failedLoginAttempts' => $blockAfterNth + 20, 'expected' => 400], + ['failedLoginAttempts' => $blockAfterNth + 50, 'expected' => 2500], + ['failedLoginAttempts' => $blockAfterNth + 60, 'expected' => 3600], + ['failedLoginAttempts' => $blockAfterNth + 61, 'expected' => 3600], + ['failedLoginAttempts' => $blockAfterNth + 100, 'expected' => 3600], + ]; + foreach ($testCases as $testCase) { + + // Act: + $actual = Authenticator::calculateSecondsToDelay( + $testCase['failedLoginAttempts'] + ); + + // Assert: + $this->assertSame($testCase['expected'], $actual, sprintf( + 'Expected %s failed login attempts to result in %s second(s), not %s.', + var_export($testCase['failedLoginAttempts'], true), + var_export($testCase['expected'], true), + var_export($actual, true) + )); + } + } + + public function testGetSecondsUntilUnblocked() + { + // Arrange: + $testCases = [[ + 'numRecentFailures' => 0, + 'mostRecentFailureAt' => null, + 'expected' => 0, + ], [ + 'numRecentFailures' => Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN - 1, // no delay yet + 'mostRecentFailureAt' => UtcTime::format('-5 seconds'), + 'expected' => 0, + ], [ + 'numRecentFailures' => Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN + 5, // a 25-second delay + 'mostRecentFailureAt' => UtcTime::format('-5 seconds'), + 'expected' => 20, + ]]; + foreach ($testCases as $testCase) { + + // Act: + $actual = Authenticator::getSecondsUntilUnblocked( + $testCase['numRecentFailures'], + $testCase['mostRecentFailureAt'] + ); + + // Assert: + $this->assertSame($testCase['expected'], $actual); + } + } + + public function testIsEnoughFailedLoginsToBlock() + { + // Arrange: + $testCases = [ + ['expected' => false, 'failedLogins' => 0], + ['expected' => false, 'failedLogins' => Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN - 1], + ['expected' => true, 'failedLogins' => Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN], + ['expected' => true, 'failedLogins' => Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN + 1], + ]; + foreach ($testCases as $testCase) { + + // Act: + $actual = Authenticator::isEnoughFailedLoginsToBlock( + $testCase['failedLogins'] + ); + + // Assert: + $this->assertSame($testCase['expected'], $actual); + } + } +} diff --git a/modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php b/modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php new file mode 100644 index 00000000..3d18f2c6 --- /dev/null +++ b/modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php @@ -0,0 +1,13 @@ +assertTrue(is_array($sspConfig), sprintf( + 'Expected an array, got this: %s', + var_export($sspConfig, true) + )); + } + + public function testGetSspConfigFor() + { + // Arrange: + $category = 'mysql'; + + // Act: + $result = ConfigManager::getSspConfigFor($category); + + // Assert: + $this->assertArrayHasKey('database', $result, var_export($result, true)); + } + + public function testRemoveCategory() + { + // Arrange: + $testCases = [ + ['key' => null, 'expected' => null], + ['key' => '', 'expected' => ''], + ['key' => '.', 'expected' => ''], + ['key' => '.abc', 'expected' => 'abc'], + ['key' => 'category.subKey', 'expected' => 'subKey'], + ['key' => 'category.subCategory.subKey', 'expected' => 'subCategory.subKey'], + ]; + foreach ($testCases as $testCase) { + + // Act: + $actual = ConfigManager::removeCategory($testCase['key']); + + // Assert: + $this->assertSame($testCase['expected'], $actual, sprintf( + 'Expected removing the category from %s result in %s, not %s.', + var_export($testCase['key'], true), + var_export($testCase['expected'], true), + var_export($actual, true) + )); + } + } +} diff --git a/modules/silauth/src/tests/unit/csrf/CsrfProtectorTest.php b/modules/silauth/src/tests/unit/csrf/CsrfProtectorTest.php new file mode 100644 index 00000000..7d6c9e4f --- /dev/null +++ b/modules/silauth/src/tests/unit/csrf/CsrfProtectorTest.php @@ -0,0 +1,26 @@ +getMasterToken(); + $firstTokenAgain = $csrfProtector->getMasterToken(); + + // Act: + $csrfProtector->changeMasterToken(); + $secondToken = $csrfProtector->getMasterToken(); + $secondTokenAgain = $csrfProtector->getMasterToken(); + + // Assert: + $this->assertSame($firstToken, $firstTokenAgain); + $this->assertNotEquals($firstToken, $secondToken); + $this->assertSame($secondToken, $secondTokenAgain); + } +} diff --git a/modules/silauth/src/tests/unit/csrf/FakeSession.php b/modules/silauth/src/tests/unit/csrf/FakeSession.php new file mode 100644 index 00000000..9c5471a8 --- /dev/null +++ b/modules/silauth/src/tests/unit/csrf/FakeSession.php @@ -0,0 +1,41 @@ +inMemoryDataStore = []; + } + + /** + * @param string $type + * @param string|null $id + * @return mixed + */ + public function getData($type, $id) + { + return $this->inMemoryDataStore[$type][$id] ?? null; + } + + public static function getSessionFromRequest($sessionId = null) + { + return new self(); + } + + public function setData($type, $id, $data, $timeout = null) + { + // Make sure an array exists for that type of data. + $this->inMemoryDataStore[$type] = $this->inMemoryDataStore[$type] ?? []; + + // Store the given data. + $this->inMemoryDataStore[$type][$id] = $data; + } +} diff --git a/modules/silauth/src/tests/unit/http/DummyRequest.php b/modules/silauth/src/tests/unit/http/DummyRequest.php new file mode 100644 index 00000000..47b9cc0f --- /dev/null +++ b/modules/silauth/src/tests/unit/http/DummyRequest.php @@ -0,0 +1,31 @@ +dummyIpAddress]; + } + + public function setDummyIpAddress($dummyIpAddress) + { + if ( ! self::isValidIpAddress($dummyIpAddress)) { + throw new \InvalidArgumentException(sprintf( + '%s is not a valid IP address.', + var_export($dummyIpAddress, true) + )); + } + + $this->dummyIpAddress = $dummyIpAddress; + } +} diff --git a/modules/silauth/src/tests/unit/http/RequestTest.php b/modules/silauth/src/tests/unit/http/RequestTest.php new file mode 100644 index 00000000..bab437ab --- /dev/null +++ b/modules/silauth/src/tests/unit/http/RequestTest.php @@ -0,0 +1,51 @@ + '11.11.11.11', + 'trustedIpAddresses' => $trustedIpAddresses, + 'expected' => true, + ], [ + 'ipAddress' => '22.22.22.22', + 'trustedIpAddresses' => $trustedIpAddresses, + 'expected' => false, + ], [ + 'ipAddress' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + 'trustedIpAddresses' => $trustedIpAddresses, + 'expected' => true, + ], [ + 'ipAddress' => '2001:0DB8:85A3:0000:0000:8A2E:0370:7334', + 'trustedIpAddresses' => $trustedIpAddresses, + 'expected' => true, + ], [ + 'ipAddress' => '2001:0db8:85a3:0000:0000:8a2e:0370:9999', + 'trustedIpAddresses' => $trustedIpAddresses, + 'expected' => false, + ]]; + foreach ($testCases as $testCase) { + $request = new Request($testCase['trustedIpAddresses']); + + // Act: + $actual = $request->isTrustedIpAddress($testCase['ipAddress']); + + // Assert: + $this->assertSame($testCase['expected'], $actual, sprintf( + 'Expected %s %sto be trusted.', + var_export($testCase['ipAddress'], true), + ($testCase['expected'] ? '' : 'not ') + )); + } + } +} diff --git a/modules/silauth/src/tests/unit/models/FailedLoginIpAddressTest.php b/modules/silauth/src/tests/unit/models/FailedLoginIpAddressTest.php new file mode 100644 index 00000000..cc2ff487 --- /dev/null +++ b/modules/silauth/src/tests/unit/models/FailedLoginIpAddressTest.php @@ -0,0 +1,189 @@ +assertTrue($model->insert(false)); + } + } + + public function testCountRecentFailedLoginsFor() + { + // Arrange: + $ipAddress = '100.110.120.130'; + $fixtures = [[ + 'ip_address' => $ipAddress, + 'occurred_at_utc' => UtcTime::format('-61 minutes'), // Not recent. + ], [ + 'ip_address' => $ipAddress, + 'occurred_at_utc' => UtcTime::format('-59 minutes'), // Recent. + ], [ + 'ip_address' => $ipAddress, + 'occurred_at_utc' => UtcTime::format(), // Now (thus, recent). + ]]; + $this->setDbFixture($fixtures); + + // Pre-assert: + $this->assertCount( + count($fixtures), + FailedLoginIpAddress::getFailedLoginsFor($ipAddress) + ); + + // Act: + $result = FailedLoginIpAddress::countRecentFailedLoginsFor($ipAddress); + + // Assert: + $this->assertEquals(2, $result); + } + + public function testGetMostRecentFailedLoginFor() + { + // Arrange: + $ipAddress = '100.110.120.130'; + $nowDateTimeString = UtcTime::now(); + $fixtures = [[ + 'ip_address' => $ipAddress, + 'occurred_at_utc' => UtcTime::format('-61 minutes'), + ], [ + 'ip_address' => $ipAddress, + 'occurred_at_utc' => $nowDateTimeString, + ], [ + 'ip_address' => $ipAddress, + 'occurred_at_utc' => UtcTime::format('-59 minutes'), + ]]; + $this->setDbFixture($fixtures); + + // Act: + $fliaRecord = FailedLoginIpAddress::getMostRecentFailedLoginFor($ipAddress); + + // Assert: + $this->assertSame($nowDateTimeString, $fliaRecord->occurred_at_utc); + } + + public function testIsCaptchaRequiredFor() + { + // Arrange: + $captchaAfterNth = Authenticator::REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN; + $testCases = [[ + 'dbFixture' => array_fill( + 0, + $captchaAfterNth, + ['ip_address' => '11.11.11.11', 'occurred_at_utc' => UtcTime::now()] + ), + 'ipAddress' => '11.11.11.11', + 'expected' => true, + ], [ + 'dbFixture' => array_fill( + 0, + $captchaAfterNth - 1, + ['ip_address' => '22.22.22.22', 'occurred_at_utc' => UtcTime::now()] + ), + 'ipAddress' => '22.22.22.22', + 'expected' => false, + ]]; + foreach ($testCases as $testCase) { + $this->setDbFixture($testCase['dbFixture']); + + // Act: + $actual = FailedLoginIpAddress::isCaptchaRequiredFor($testCase['ipAddress']); + + // Assert: + $this->assertSame($testCase['expected'], $actual); + } + } + + public function testIsRateLimitBlocking() + { + // Arrange: + $blockAfterNth = Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN; + $testCases = [[ + 'dbFixture' => array_fill( + 0, + $blockAfterNth, + ['ip_address' => '11.11.11.11', 'occurred_at_utc' => UtcTime::now()] + ), + 'ipAddress' => '11.11.11.11', + 'expected' => true, + ], [ + 'dbFixture' => array_fill( + 0, + $blockAfterNth - 1, + ['ip_address' => '22.22.22.22', 'occurred_at_utc' => UtcTime::now()] + ), + 'ipAddress' => '22.22.22.22', + 'expected' => false, + ]]; + foreach ($testCases as $testCase) { + $this->setDbFixture($testCase['dbFixture']); + + // Act: + $actual = FailedLoginIpAddress::isRateLimitBlocking($testCase['ipAddress']); + + // Assert: + $this->assertSame($testCase['expected'], $actual); + } + } + + public function testRecordFailedLoginBy() + { + // Arrange: + $ipAddress = '101.102.103.104'; + $dbFixture = [ + ['ip_address' => $ipAddress, 'occurred_at_utc' => UtcTime::format()] + ]; + $this->setDbFixture($dbFixture); + $logger = new Psr3EchoLogger(); + $expectedPre = count($dbFixture); + $expectedPost = $expectedPre + 1; + + // Pre-assert: + $this->assertCount( + $expectedPre, + FailedLoginIpAddress::getFailedLoginsFor($ipAddress) + ); + + // Act: + FailedLoginIpAddress::recordFailedLoginBy([$ipAddress], $logger); + + // Assert: + $this->assertCount( + $expectedPost, + FailedLoginIpAddress::getFailedLoginsFor($ipAddress) + ); + } + + public function testResetFailedLoginsBy() + { + // Arrange: + $ipAddress = '101.102.103.104'; + $otherIpAddress = '201.202.203.204'; + $logger = new Psr3EchoLogger(); + FailedLoginIpAddress::deleteAll(); + FailedLoginIpAddress::recordFailedLoginBy( + [$ipAddress, $otherIpAddress], + $logger + ); + + // Pre-assert: + $this->assertCount(1, FailedLoginIpAddress::getFailedLoginsFor($ipAddress)); + $this->assertCount(1, FailedLoginIpAddress::getFailedLoginsFor($otherIpAddress)); + + // Act: + FailedLoginIpAddress::resetFailedLoginsBy([$ipAddress]); + + // Assert: + $this->assertCount(0, FailedLoginIpAddress::getFailedLoginsFor($ipAddress)); + $this->assertCount(1, FailedLoginIpAddress::getFailedLoginsFor($otherIpAddress)); + } +} diff --git a/modules/silauth/src/tests/unit/models/FailedLoginUsernameTest.php b/modules/silauth/src/tests/unit/models/FailedLoginUsernameTest.php new file mode 100644 index 00000000..6e1e0a7b --- /dev/null +++ b/modules/silauth/src/tests/unit/models/FailedLoginUsernameTest.php @@ -0,0 +1,188 @@ +assertTrue($model->insert(false)); + } + } + + public function testCountRecentFailedLoginsFor() + { + // Arrange: + $username = 'john_smith'; + $fixtures = [[ + 'username' => $username, + 'occurred_at_utc' => UtcTime::format('-61 minutes'), // Not recent. + ], [ + 'username' => $username, + 'occurred_at_utc' => UtcTime::format('-59 minutes'), // Recent. + ], [ + 'username' => $username, + 'occurred_at_utc' => UtcTime::format(), // Now (thus, recent). + ]]; + $this->setDbFixture($fixtures); + + // Pre-assert: + $this->assertCount( + count($fixtures), + FailedLoginUsername::getFailedLoginsFor($username) + ); + + // Act: + $result = FailedLoginUsername::countRecentFailedLoginsFor($username); + + // Assert: + $this->assertEquals(2, $result); + } + + public function testGetMostRecentFailedLoginFor() + { + // Arrange: + $username = 'dummy_username'; + $nowDateTimeString = UtcTime::now(); + $fixtures = [[ + 'username' => $username, + 'occurred_at_utc' => UtcTime::format('-61 minutes'), + ], [ + 'username' => $username, + 'occurred_at_utc' => $nowDateTimeString, + ], [ + 'username' => $username, + 'occurred_at_utc' => UtcTime::format('-59 minutes'), + ]]; + $this->setDbFixture($fixtures); + + // Act: + $fliaRecord = FailedLoginUsername::getMostRecentFailedLoginFor($username); + + // Assert: + $this->assertSame($nowDateTimeString, $fliaRecord->occurred_at_utc); + } + + public function testIsCaptchaRequiredFor() + { + // Arrange: + $captchaAfterNth = Authenticator::REQUIRE_CAPTCHA_AFTER_NTH_FAILED_LOGIN; + $testCases = [[ + 'dbFixture' => array_fill( + 0, + $captchaAfterNth, + ['username' => 'dummy_username', 'occurred_at_utc' => UtcTime::now()] + ), + 'username' => 'dummy_username', + 'expected' => true, + ], [ + 'dbFixture' => array_fill( + 0, + $captchaAfterNth - 1, + ['username' => 'dummy_other_username', 'occurred_at_utc' => UtcTime::now()] + ), + 'username' => 'dummy_other_username', + 'expected' => false, + ]]; + foreach ($testCases as $testCase) { + $this->setDbFixture($testCase['dbFixture']); + + // Act: + $actual = FailedLoginUsername::isCaptchaRequiredFor($testCase['username']); + + // Assert: + $this->assertSame($testCase['expected'], $actual); + } + } + + public function testIsRateLimitBlocking() + { + // Arrange: + $blockAfterNth = Authenticator::BLOCK_AFTER_NTH_FAILED_LOGIN; + $testCases = [[ + 'dbFixture' => array_fill( + 0, + $blockAfterNth, + ['username' => 'dummy_username', 'occurred_at_utc' => UtcTime::now()] + ), + 'username' => 'dummy_username', + 'expected' => true, + ], [ + 'dbFixture' => array_fill( + 0, + $blockAfterNth - 1, + ['username' => 'dummy_other_username', 'occurred_at_utc' => UtcTime::now()] + ), + 'username' => 'dummy_other_username', + 'expected' => false, + ]]; + foreach ($testCases as $testCase) { + $this->setDbFixture($testCase['dbFixture']); + + // Act: + $actual = FailedLoginUsername::isRateLimitBlocking($testCase['username']); + + // Assert: + $this->assertSame($testCase['expected'], $actual); + } + } + + public function testRecordFailedLoginBy() + { + // Arrange: + $username = 'dummy_username'; + $dbFixture = [ + ['username' => $username, 'occurred_at_utc' => UtcTime::format()] + ]; + $this->setDbFixture($dbFixture); + $logger = new Psr3EchoLogger(); + $expectedPre = count($dbFixture); + $expectedPost = $expectedPre + 1; + + // Pre-assert: + $this->assertCount( + $expectedPre, + FailedLoginUsername::getFailedLoginsFor($username) + ); + + // Act: + FailedLoginUsername::recordFailedLoginBy($username, $logger); + + // Assert: + $this->assertCount( + $expectedPost, + FailedLoginUsername::getFailedLoginsFor($username) + ); + } + + public function testResetFailedLoginsBy() + { + // Arrange: + $username = 'dummy_username'; + $otherUsername = 'dummy_other_username'; + $dbFixture = [ + ['username' => $username, 'occurred_at_utc' => UtcTime::format()], + ['username' => $otherUsername, 'occurred_at_utc' => UtcTime::format()], + ]; + $this->setDbFixture($dbFixture); + + // Pre-assert: + $this->assertCount(1, FailedLoginUsername::getFailedLoginsFor($username)); + $this->assertCount(1, FailedLoginUsername::getFailedLoginsFor($otherUsername)); + + // Act: + FailedLoginUsername::resetFailedLoginsBy($username); + + // Assert: + $this->assertCount(0, FailedLoginUsername::getFailedLoginsFor($username)); + $this->assertCount(1, FailedLoginUsername::getFailedLoginsFor($otherUsername)); + } +} diff --git a/modules/silauth/src/tests/unit/text/TextTest.php b/modules/silauth/src/tests/unit/text/TextTest.php new file mode 100644 index 00000000..3236d935 --- /dev/null +++ b/modules/silauth/src/tests/unit/text/TextTest.php @@ -0,0 +1,42 @@ + '', 'expected' => ''], + ['input' => null, 'expected' => ''], + ['input' => false, 'expected' => ''], + ['input' => true, 'expected' => ''], + ['input' => 'null', 'expected' => 'null'], + ['input' => 'false', 'expected' => 'false'], + ['input' => 'true', 'expected' => 'true'], + ['input' => 'abc XYZ', 'expected' => 'abc XYZ'], + ['input' => ' leading space', 'expected' => 'leading space'], + ['input' => 'trailing space ', 'expected' => 'trailing space'], + ['input' => 'trailing space ', 'expected' => 'trailing space'], + ['input' => 'low ASCII char: ' . chr(2), 'expected' => 'low ASCII char:'], + ['input' => 'high ASCII char: ' . chr(160), 'expected' => 'high ASCII char: ' . chr(160)], + ['input' => 'with `backticks`', 'expected' => 'with backticks'], + ]; + foreach ($testCases as $testCase) { + + // Act: + $actual = Text::sanitizeString($testCase['input']); + + // Assert: + $this->assertSame($testCase['expected'], $actual, sprintf( + 'Expected sanitizing %s to result in %s, not %s.', + var_export($testCase['input'], true), + var_export($testCase['expected'], true), + var_export($actual, true) + )); + } + } +} diff --git a/modules/silauth/src/tests/unit/time/UtcTimeTest.php b/modules/silauth/src/tests/unit/time/UtcTimeTest.php new file mode 100644 index 00000000..13ff2c72 --- /dev/null +++ b/modules/silauth/src/tests/unit/time/UtcTimeTest.php @@ -0,0 +1,167 @@ + '1 Jan 2000 00:00:00 -0000', + 'expected' => '2000-01-01 00:00:00', + ], [ + 'dateTimeString' => '2016-Dec-25 12:00pm', + 'expected' => '2016-12-25 12:00:00', + ], + ]; + foreach ($testCases as $testCase) { + + // Act: + $actual = UtcTime::format($testCase['dateTimeString']); + + // Assert: + $this->assertSame($testCase['expected'], $actual); + } + } + + public function testGetRemainingSeconds() + { + // Arrange: + $testCases = [ + ['total' => 1, 'elapsed' => null, 'expectException' => '\TypeError'], + ['total' => null, 'elapsed' => 1, 'expectException' => '\TypeError'], + ['total' => 1, 'elapsed' => '1', 'expectException' => '\TypeError'], + ['total' => '1', 'elapsed' => 1, 'expectException' => '\TypeError'], + ['total' => -1, 'elapsed' => 1, 'expected' => 0], + ['total' => -1, 'elapsed' => 0, 'expected' => 0], + ['total' => 0, 'elapsed' => 0, 'expected' => 0], + ['total' => 0, 'elapsed' => 5, 'expected' => 0], + ['total' => 5, 'elapsed' => 0, 'expected' => 5], + ['total' => 5, 'elapsed' => 5, 'expected' => 0], + ['total' => 8, 'elapsed' => 5, 'expected' => 3], + ['total' => 60, 'elapsed' => 45, 'expected' => 15], + ]; + foreach ($testCases as $testCase) { + $total = $testCase['total']; + $elapsed = $testCase['elapsed']; + $expected = $testCase['expected'] ?? null; + $expectException = $testCase['expectException'] ?? null; + + // Pre-assert: + if ($expectException !== null) { + $this->expectException($expectException); + } + + // Act: + $actual = UtcTime::getRemainingSeconds($total, $elapsed); + + // Assert: + if ($expectException !== null) { + $this->fail(sprintf( + 'Expected a %s to be thrown for (total: %s, elapsed: %s).', + $expectException, + var_export($total, true), + var_export($elapsed, true) + )); + } + $this->assertSame($expected, $actual, sprintf( + 'Expected (total: %s, elapsed: %s) to result in %s, not %s.', + var_export($total, true), + var_export($elapsed, true), + var_export($expected, true), + var_export($actual, true) + )); + } + } + + public function testGetSecondsSinceDateTime() + { + // Arrange: + $testCases = [ + ['value' => '1970-01-01 00:00:00', 'expected' => time()], + ['value' => UtcTime::format(), 'expected' => 0], + ['value' => UtcTime::format('-10 seconds'), 'expected' => 10], + ['value' => UtcTime::format('-2 hours'), 'expected' => 7200], + ]; + foreach ($testCases as $testCase) { + + // Act: + $actual = UtcTime::getSecondsSinceDateTime($testCase['value']); + + // Assert: + $this->assertEqualsWithDelta( + $testCase['expected'], + $actual, + 1, + sprintf('Expected %s to result in %s, not %s.', + var_export($testCase['value'], true), + var_export($testCase['expected'], true), + var_export($actual, true) + ) + ); + } + } + + public function testGetSecondsSinceDateTimeEmptyString() + { + $this->expectException('\InvalidArgumentException'); + UtcTime::getSecondsSinceDateTime(''); + } + + public function testGetSecondsSinceDateTimeInvalidDateTimeString() + { + $this->expectException('\Exception'); + UtcTime::getSecondsSinceDateTime('asdf'); + } + + public function testGetSecondsSinceDateTimeNull() + { + $this->expectException('\TypeError'); + UtcTime::getSecondsSinceDateTime(null); + } + + public function testGetSecondsUntil() + { + // Arrange: + $dayOneString = 'Tue, 13 Dec 2016 00:00:00 -0500'; + $dayTwoString = 'Wed, 14 Dec 2016 00:00:00 -0500'; + $expected = 86400; // 86400 = seconds in a day + $dayOneUtcTime = new UtcTime($dayOneString); + $dayTwoUtcTime = new UtcTime($dayTwoString); + + // Act: + $actual = $dayOneUtcTime->getSecondsUntil($dayTwoUtcTime); + + // Assert: + $this->assertSame($expected, $actual); + } + + public function testGetTimestamp() + { + // Arrange: + $timestamp = time(); + $utcTime = new UtcTime(date('r', $timestamp)); + + // Act: + $result = $utcTime->getTimestamp(); + + // Assert: + $this->assertSame($timestamp, $result); + } + + public function testNow() + { + // Arrange: + $expected = gmdate(UtcTime::DATE_TIME_FORMAT, time()); + + // Act: + $actual = UtcTime::now(); + + // Assert: + $this->assertSame($expected, $actual); + } +} diff --git a/modules/silauth/src/tests/unit/time/WaitTimeTest.php b/modules/silauth/src/tests/unit/time/WaitTimeTest.php new file mode 100644 index 00000000..3be7843d --- /dev/null +++ b/modules/silauth/src/tests/unit/time/WaitTimeTest.php @@ -0,0 +1,73 @@ + 0, 'expected' => '5 seconds'], + ['secondsToWait' => 1, 'expected' => '5 seconds'], + ['secondsToWait' => 5, 'expected' => '5 seconds'], + ['secondsToWait' => 6, 'expected' => '10 seconds'], + ['secondsToWait' => 17, 'expected' => '20 seconds'], + ['secondsToWait' => 22, 'expected' => '30 seconds'], + ['secondsToWait' => 41, 'expected' => '1 minute'], + ['secondsToWait' => 90, 'expected' => '2 minutes'], + ['secondsToWait' => 119, 'expected' => '2 minutes'], + ['secondsToWait' => 120, 'expected' => '2 minutes'], + ['secondsToWait' => 121, 'expected' => '3 minutes'], + ]; + foreach ($testCases as $testCase) { + $waitTime = new WaitTime($testCase['secondsToWait']); + + // Act: + $actual = (string)$waitTime; + + // Assert: + $this->assertSame($testCase['expected'], $actual, sprintf( + 'Expected %s second(s) to result in %s, not %s.', + var_export($testCase['secondsToWait'], true), + var_export($testCase['expected'], true), + var_export($actual, true) + )); + } + } + + public function testGetLongestWaitTime() + { + // Arrange: + $testCases = [ + ['durations' => [], 'expectException' => '\InvalidArgumentException'], + ['durations' => [0, 0], 'expected' => new WaitTime(0)], + ['durations' => [0, 1], 'expected' => new WaitTime(1)], + ['durations' => [1, 0], 'expected' => new WaitTime(1)], + ['durations' => [6], 'expected' => new WaitTime(6)], + ['durations' => [5, 5, 6], 'expected' => new WaitTime(6)], + ['durations' => [5, 6, 5], 'expected' => new WaitTime(6)], + ['durations' => [6, 5, 5], 'expected' => new WaitTime(6)], + ['durations' => [0, 17], 'expected' => new WaitTime(17)], + ['durations' => [17, 5], 'expected' => new WaitTime(17)], + ]; + foreach ($testCases as $testCase) { + if (array_key_exists('expectException', $testCase)) { + $this->expectException($testCase['expectException']); + } + + // Act: + $actual = WaitTime::getLongestWaitTime($testCase['durations']); + + // Assert: + $this->assertEquals($testCase['expected'], $actual, sprintf( + 'Expected the longest of %s second(s) to be a wait time of %s, not %s.', + json_encode($testCase['durations']), + $testCase['expected'], + $actual + )); + } + } +} diff --git a/modules/silauth/src/text/Text.php b/modules/silauth/src/text/Text.php new file mode 100644 index 00000000..824c101f --- /dev/null +++ b/modules/silauth/src/text/Text.php @@ -0,0 +1,34 @@ + FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_BACKTICK, + ]); + return trim($output); + } + + /** + * See if the given string (haystack) starts with the given prefix (needle). + * + * @param string $haystack The string to search. + * @param string $needle The string to search for. + * @return boolean + */ + public static function startsWith(string $haystack, string $needle) + { + $length = mb_strlen($needle); + return (mb_substr($haystack, 0, $length) === $needle); + } +} diff --git a/modules/silauth/src/time/UtcTime.php b/modules/silauth/src/time/UtcTime.php new file mode 100644 index 00000000..94465606 --- /dev/null +++ b/modules/silauth/src/time/UtcTime.php @@ -0,0 +1,120 @@ +. + * @throws Exception If an invalid date/time string is provided, an + * \Exception will be thrown. + */ + public function __construct(string $dateTimeString = 'now') + { + $this->utc = new \DateTimeZone('UTC'); + $this->dateTime = new \DateTime($dateTimeString, $this->utc); + } + + public function __toString() + { + return $this->dateTime->format(self::DATE_TIME_FORMAT); + } + + /** + * Convert the given date/time description to a formatted date/time string + * in the UTC time zone. + * + * @param string $dateTimeString (Optional:) The date/time to use. If not + * given, 'now' will be used. + * @return string + * @throws Exception If an invalid date/time string is provided, an + * \Exception will be thrown. + */ + public static function format(string $dateTimeString = 'now') + { + return (string)(new UtcTime($dateTimeString)); + } + + /** + * Given a total number of seconds and an elapsed number of seconds, get the + * remaining seconds until that total has passed. If the total has already + * passed (i.e. if elapsed is equal to or greater than total), zero will be + * returned. + * + * @param int $totalSeconds The total number of seconds. + * @param int $elapsedSeconds The number of seconds that have already + * passed. + * @return int The number of seconds remaining. + */ + public static function getRemainingSeconds(int $totalSeconds, int $elapsedSeconds) + { + $remainingSeconds = $totalSeconds - $elapsedSeconds; + return max($remainingSeconds, 0); + } + + /** + * Get the number of seconds we have to go back to get from this UTC time to + * the given UTC time. A positive number will be returned if the given UTC + * time occurred before this UTC time. If they are the same, zero will be + * returned. Otherwise, a negative number will be returned. + * + * @param \Sil\SilAuth\time\UtcTime $otherUtcTime The other UTC time + * (presumably in the past, though not necessarily). + * @return int The number of seconds + */ + public function getSecondsSince(UtcTime $otherUtcTime) + { + return $this->getTimestamp() - $otherUtcTime->getTimestamp(); + } + + /** + * Get the number of seconds since the given date/time string. + * + * @param string $dateTimeString A date/time string. + * @return int The number of seconds that have elapsed since that date/time. + * @throws Exception If an invalid date/time string is provided, an + * \Exception will be thrown. + * @throws \InvalidArgumentException + */ + public static function getSecondsSinceDateTime(string $dateTimeString) + { + if (empty($dateTimeString)) { + throw new \InvalidArgumentException(sprintf( + 'The given value (%s) is not a date/time string.', + var_export($dateTimeString, true) + )); + } + $nowUtc = new UtcTime(); + $dateTimeUtc = new UtcTime($dateTimeString); + return $nowUtc->getSecondsSince($dateTimeUtc); + } + + public function getSecondsUntil(UtcTime $otherUtcTime) + { + return $otherUtcTime->getTimestamp() - $this->getTimestamp(); + } + + public function getTimestamp() + { + return $this->dateTime->getTimestamp(); + } + + /** + * Get the current date/time (UTC) as a formatted string + * + * @return string + */ + public static function now() + { + return self::format('now'); + } +} diff --git a/modules/silauth/src/time/WaitTime.php b/modules/silauth/src/time/WaitTime.php new file mode 100644 index 00000000..00caf2e7 --- /dev/null +++ b/modules/silauth/src/time/WaitTime.php @@ -0,0 +1,73 @@ +friendlyNumber = 5; + $this->unit = self::UNIT_SECOND; + } elseif ($secondsToWait <= 30) { + $this->friendlyNumber = (int)ceil($secondsToWait / 10) * 10; + $this->unit = self::UNIT_SECOND; + } else { + $this->friendlyNumber = (int)ceil($secondsToWait / 60); + $this->unit = self::UNIT_MINUTE; + } + } + + public function getFriendlyNumber() + { + return $this->friendlyNumber; + } + + /** + * Get a WaitTime representing the longest of the given durations. + * + * @param int[] $durationsInSeconds A list of (at least one) duration(s), in + * seconds. + * @return WaitTime + */ + public static function getLongestWaitTime(array $durationsInSeconds) + { + if (empty($durationsInSeconds)) { + throw new \InvalidArgumentException('No durations given.', 1487605801); + } + return new WaitTime(max($durationsInSeconds)); + } + + public function getUnit() + { + return $this->unit; + } + + public function __toString() + { + return sprintf( + '%s %s%s', + $this->friendlyNumber, + $this->unit, + (($this->friendlyNumber === 1) ? '' : 's') + ); + } +} diff --git a/modules/silauth/src/traits/LoggerAwareTrait.php b/modules/silauth/src/traits/LoggerAwareTrait.php new file mode 100644 index 00000000..555af812 --- /dev/null +++ b/modules/silauth/src/traits/LoggerAwareTrait.php @@ -0,0 +1,29 @@ +logger)) { + $this->logger = new NullLogger(); + } + } + + /** + * Set a logger for this class instance to use. + * + * @param LoggerInterface $logger A PSR-3 compliant logger. + * @return null + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } +} diff --git a/modules/silauth/src/yii b/modules/silauth/src/yii new file mode 100755 index 00000000..d86131f8 --- /dev/null +++ b/modules/silauth/src/yii @@ -0,0 +1,43 @@ +#!/usr/bin/env php + ['db' => [ + 'dsn' => sprintf( + 'mysql:host=%s;dbname=%s', + Env::get('MYSQL_HOST'), + Env::get('MYSQL_DATABASE') + ), + 'username' => Env::get('MYSQL_USER'), + 'password' => Env::get('MYSQL_PASSWORD'), +]]]); +$exitCode = $application->run(); +exit($exitCode); diff --git a/modules/silauth/www/loginuserpass.php b/modules/silauth/www/loginuserpass.php new file mode 100644 index 00000000..c1b4ccb2 --- /dev/null +++ b/modules/silauth/www/loginuserpass.php @@ -0,0 +1,108 @@ +getConfig('authsources.php'); +$silAuthConfig = $authSourcesConfig->getConfigItem('silauth'); + +$recaptchaSiteKey = $silAuthConfig->getString('recaptcha.siteKey', null); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + try { + + $logger = new Psr3StdOutLogger(); + $csrfFromRequest = Request::sanitizeInputString(INPUT_POST, 'csrf-token'); + if ($csrfProtector->isTokenCorrect($csrfFromRequest)) { + + $username = Request::sanitizeInputString(INPUT_POST, 'username'); + $password = Request::getRawInputString(INPUT_POST, 'password'); + + SilAuth::handleLogin( + $authStateId, + $username, + $password + ); + } else { + $logger->error(json_encode([ + 'event' => 'Failed CSRF', + 'username' => Request::sanitizeInputString(INPUT_POST, 'username'), + 'userAgent' => Request::getUserAgent(), + ])); + } + + } catch (SimpleSAMLError $e) { + /* Login failed. Extract error code and parameters, to display the error. */ + $errorCode = $e->getErrorCode(); + $errorParams = $e->getParameters(); + } + + $csrfProtector->changeMasterToken(); +} + +$t = new Template($globalConfig, 'core:loginuserpass.php'); +$t->data['stateparams'] = array('AuthState' => $authStateId); +$t->data['username'] = $username; +$t->data['forceUsername'] = false; +$t->data['rememberUsernameEnabled'] = false; +$t->data['rememberMeEnabled'] = false; +$t->data['errorcode'] = $errorCode; +$t->data['errorparams'] = $errorParams; +$t->data['csrfToken'] = $csrfProtector->getMasterToken(); +$t->data['profileUrl'] = $state['templateData']['profileUrl'] ?? ''; +$t->data['helpCenterUrl'] = $state['templateData']['helpCenterUrl'] ?? ''; + +/* For simplicity's sake, don't bother telling this Request to trust any IP + * addresses. This is okay because we only track the failures of untrusted + * IP addresses, so there will be no failed logins of IP addresses we trust. */ +$request = new Request(); +if (Authenticator::isCaptchaRequired($username, $request->getUntrustedIpAddresses())) { + $t->data['recaptcha.siteKey'] = $recaptchaSiteKey; +} + +if (isset($state['SPMetadata'])) { + $t->data['SPMetadata'] = $state['SPMetadata']; +} else { + $t->data['SPMetadata'] = null; +} + +$t->show(); +exit(); diff --git a/modules/silauth/www/status.php b/modules/silauth/www/status.php new file mode 100644 index 00000000..22570022 --- /dev/null +++ b/modules/silauth/www/status.php @@ -0,0 +1,32 @@ + ['db' => [ + 'dsn' => sprintf( + 'mysql:host=%s;dbname=%s', + Env::get('MYSQL_HOST'), + Env::get('MYSQL_DATABASE') + ), + 'username' => Env::get('MYSQL_USER'), + 'password' => Env::get('MYSQL_PASSWORD'), + ]]]); + $logger = new Psr3StdOutLogger(); + $system = new System($logger); + $system->reportStatus(); + +} catch (Throwable $t) { + + echo sprintf( + '%s (%s)', + $t->getMessage(), + $t->getCode() + ); + \http_response_code(500); +} From f4b2d6ea43aa9ad59f4f332ddba2fe0b9895d774 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 13 May 2024 11:04:30 +0800 Subject: [PATCH 34/92] use the local silauth module and remove external module from composer --- composer.json | 1 - composer.lock | 1038 ++------------------------------------------ docker-compose.yml | 7 + 3 files changed, 35 insertions(+), 1011 deletions(-) diff --git a/composer.json b/composer.json index b5a9031d..131fb723 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "ext-memcached": "*", "simplesamlphp/simplesamlphp": "^1.19.6", "simplesamlphp/composer-module-installer": "1.1.8", - "silinternational/simplesamlphp-module-silauth": "^7.1.1", "silinternational/ssp-utilities": "^1.1.0", "silinternational/simplesamlphp-module-material": "^8.1.1", "silinternational/simplesamlphp-module-sildisco": "^4.0.0", diff --git a/composer.lock b/composer.lock index 027ae04b..08ce7361 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": "69b009959cbdf313f49a8d5029e8bffe", + "content-hash": "8f7f00c703ea40df4fbdcf105af4b017", "packages": [ { "name": "aws/aws-crt-php", @@ -155,216 +155,6 @@ }, "time": "2023-04-26T18:21:04+00:00" }, - { - "name": "cebe/markdown", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/cebe/markdown.git", - "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86", - "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86", - "shasum": "" - }, - "require": { - "lib-pcre": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "cebe/indent": "*", - "facebook/xhprof": "*@dev", - "phpunit/phpunit": "4.1.*" - }, - "bin": [ - "bin/markdown" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "cebe\\markdown\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", - "role": "Creator" - } - ], - "description": "A super fast, highly extensible markdown parser for PHP", - "homepage": "https://github.com/cebe/markdown#readme", - "keywords": [ - "extensible", - "fast", - "gfm", - "markdown", - "markdown-extra" - ], - "support": { - "issues": "https://github.com/cebe/markdown/issues", - "source": "https://github.com/cebe/markdown" - }, - "time": "2018-03-26T11:24:36+00:00" - }, - { - "name": "codemix/yii2-streamlog", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/codemix/yii2-streamlog.git", - "reference": "1ed104f4bc4e961d1d08fb92d6854f01b1bd0b80" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/codemix/yii2-streamlog/zipball/1ed104f4bc4e961d1d08fb92d6854f01b1bd0b80", - "reference": "1ed104f4bc4e961d1d08fb92d6854f01b1bd0b80", - "shasum": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "codemix\\streamlog\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Härtl", - "email": "haertl.mike@gmail.com" - } - ], - "description": "A Yii 2 log target for streams in URL format", - "keywords": [ - "log", - "stdout", - "stream", - "yii2" - ], - "support": { - "issues": "https://github.com/codemix/yii2-streamlog/issues", - "source": "https://github.com/codemix/yii2-streamlog/tree/1.3.1" - }, - "time": "2020-03-26T13:23:28+00:00" - }, - { - "name": "ezyang/htmlpurifier", - "version": "v4.16.0", - "source": { - "type": "git", - "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", - "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", - "shasum": "" - }, - "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" - }, - "require-dev": { - "cerdic/css-tidy": "^1.7 || ^2.0", - "simpletest/simpletest": "dev-master" - }, - "suggest": { - "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", - "ext-bcmath": "Used for unit conversion and imagecrash protection", - "ext-iconv": "Converts text to and from non-UTF-8 encodings", - "ext-tidy": "Used for pretty-printing HTML" - }, - "type": "library", - "autoload": { - "files": [ - "library/HTMLPurifier.composer.php" - ], - "psr-0": { - "HTMLPurifier": "library/" - }, - "exclude-from-classmap": [ - "/library/HTMLPurifier/Language/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1-or-later" - ], - "authors": [ - { - "name": "Edward Z. Yang", - "email": "admin@htmlpurifier.org", - "homepage": "http://ezyang.com" - } - ], - "description": "Standards compliant HTML filter written in PHP", - "homepage": "http://htmlpurifier.org/", - "keywords": [ - "html" - ], - "support": { - "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" - }, - "time": "2022-09-18T07:06:19+00:00" - }, - { - "name": "fillup/fake-bower-assets", - "version": "2.0.13", - "source": { - "type": "git", - "url": "https://github.com/fillup/fake-bower-assets.git", - "reference": "05ca2ac45757cee3906d6056c9623585c1eea68b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fillup/fake-bower-assets/zipball/05ca2ac45757cee3906d6056c9623585c1eea68b", - "reference": "05ca2ac45757cee3906d6056c9623585c1eea68b", - "shasum": "" - }, - "replace": { - "bower-asset/bootstrap": "3.3.6", - "bower-asset/inputmask": "3.2.2", - "bower-asset/jquery": "2.2.2", - "bower-asset/jquery.inputmask": "3.2.2", - "bower-asset/punycode": "1.3.1", - "bower-asset/typeahead.js": "0.11.1", - "bower-asset/yii2-pjax": "2.0.1", - "yiisoft/yii2-bootstrap": "2.0.6" - }, - "type": "metapackage", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phillip Shipley", - "email": "phillip.shipley@gmail.com" - } - ], - "description": "Use Composer \"replace\" to fake out installing bower-asset dependencies that aren't really needed", - "support": { - "issues": "https://github.com/fillup/fake-bower-assets/issues", - "source": "https://github.com/fillup/fake-bower-assets/tree/master" - }, - "time": "2018-05-08T17:44:00+00:00" - }, { "name": "gettext/gettext", "version": "4.x-dev", @@ -520,58 +310,6 @@ ], "time": "2022-10-18T15:00:10+00:00" }, - { - "name": "google/recaptcha", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/google/recaptcha.git", - "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/google/recaptcha/zipball/d59a801e98a4e9174814a6d71bbc268dff1202df", - "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df", - "shasum": "" - }, - "require": { - "php": ">=8" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.14", - "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^10" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "psr-4": { - "ReCaptcha\\": "src/ReCaptcha" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.", - "homepage": "https://www.google.com/recaptcha/", - "keywords": [ - "Abuse", - "captcha", - "recaptcha", - "spam" - ], - "support": { - "forum": "https://groups.google.com/forum/#!forum/recaptcha", - "issues": "https://github.com/google/recaptcha/issues", - "source": "https://github.com/google/recaptcha" - }, - "time": "2023-02-18T17:41:46+00:00" - }, { "name": "guzzlehttp/command", "version": "1.3.0", @@ -1218,92 +956,6 @@ ], "time": "2022-01-13T18:05:33+00:00" }, - { - "name": "monolog/monolog", - "version": "1.27.1", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "904713c5929655dc9b97288b69cfeedad610c9a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1", - "reference": "904713c5929655dc9b97288b69cfeedad610c9a1", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" - }, - "provide": { - "psr/log-implementation": "1.0.0" - }, - "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", - "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "php-amqplib/php-amqplib": "~2.4", - "php-console/php-console": "^3.1.3", - "phpstan/phpstan": "^0.12.59", - "phpunit/phpunit": "~4.5", - "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "^5.3|^6.0" - }, - "suggest": { - "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", - "doctrine/couchdb": "Allow sending log messages to a CouchDB server", - "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", - "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "php-console/php-console": "Allow sending log messages to Google Chrome", - "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" - }, - "type": "library", - "autoload": { - "psr-4": { - "Monolog\\": "src/Monolog" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "http://github.com/Seldaek/monolog", - "keywords": [ - "log", - "logging", - "psr-3" - ], - "support": { - "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/1.27.1" - }, - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", - "type": "tidelift" - } - ], - "time": "2022-06-09T08:53:42+00:00" - }, { "name": "mtdowling/jmespath.php", "version": "2.6.1", @@ -1365,56 +1017,6 @@ }, "time": "2021-06-14T00:11:39+00:00" }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "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" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, { "name": "phpfastcache/riak-client", "version": "3.4.3", @@ -1571,47 +1173,6 @@ ], "time": "2023-03-06T14:43:22+00:00" }, - { - "name": "phpspec/php-diff", - "version": "v1.1.3", - "source": { - "type": "git", - "url": "https://github.com/phpspec/php-diff.git", - "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/fc1156187f9f6c8395886fe85ed88a0a245d72e9", - "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9", - "shasum": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Diff": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Chris Boulton", - "homepage": "http://github.com/chrisboulton" - } - ], - "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", - "support": { - "source": "https://github.com/phpspec/php-diff/tree/v1.1.3" - }, - "time": "2020-09-18T13:47:07+00:00" - }, { "name": "psr/cache", "version": "2.0.0", @@ -1912,197 +1473,56 @@ "keywords": [ "log", "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, - "time": "2021-05-03T11:20:27+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "ramsey/uuid", - "version": "3.9.7", - "source": { - "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "dc75aa439eb4c1b77f5379fd958b3dc0e6014178" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/dc75aa439eb4c1b77f5379fd958b3dc0e6014178", - "reference": "dc75aa439eb4c1b77f5379fd958b3dc0e6014178", - "shasum": "" - }, - "require": { - "ext-json": "*", - "paragonie/random_compat": "^1 | ^2 | ^9.99.99", - "php": "^5.4 | ^7.0 | ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "replace": { - "rhumsaa/uuid": "self.version" - }, - "require-dev": { - "codeception/aspect-mock": "^1 | ^2", - "doctrine/annotations": "^1.2", - "goaop/framework": "1.0.0-alpha.2 | ^1 | >=2.1.0 <=2.3.2", - "mockery/mockery": "^0.9.11 | ^1", - "moontoast/math": "^1.1", - "nikic/php-parser": "<=4.5.0", - "paragonie/random-lib": "^2", - "php-mock/php-mock-phpunit": "^0.3 | ^1.1 | ^2.6", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpunit/phpunit": ">=4.8.36 <9.0.0 | >=9.3.0", - "squizlabs/php_codesniffer": "^3.5", - "yoast/phpunit-polyfills": "^1.0" - }, - "suggest": { - "ext-ctype": "Provides support for PHP Ctype functions", - "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", - "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator", - "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", - "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", - "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Ramsey\\Uuid\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - }, - { - "name": "Marijn Huizendveld", - "email": "marijn.huizendveld@gmail.com" - }, - { - "name": "Thibaud Fabre", - "email": "thibaud@aztech.io" - } - ], - "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", - "homepage": "https://github.com/ramsey/uuid", - "keywords": [ - "guid", - "identifier", - "uuid" - ], - "support": { - "issues": "https://github.com/ramsey/uuid/issues", - "rss": "https://github.com/ramsey/uuid/releases.atom", - "source": "https://github.com/ramsey/uuid", - "wiki": "https://github.com/ramsey/uuid/wiki" - }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } + "psr-3" ], - "time": "2022-12-19T21:55:10+00:00" + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" }, { - "name": "rlanvin/php-ip", - "version": "v1.0.1", + "name": "ralouphie/getallheaders", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/rlanvin/php-ip.git", - "reference": "03e507114b95e2d3a9b9621dc6fb583401afbf5e" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rlanvin/php-ip/zipball/03e507114b95e2d3a9b9621dc6fb583401afbf5e", - "reference": "03e507114b95e2d3a9b9621dc6fb583401afbf5e", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "ext-gmp": "*", - "php": ">=5.3.0" + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, "type": "library", "autoload": { - "classmap": [ - "src/" + "files": [ + "src/getallheaders.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "IPv4/IPv6 manipulation library for PHP", - "homepage": "https://github.com/rlanvin/php-ip", - "keywords": [ - "IP", - "ipv4", - "ipv6" + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } ], + "description": "A polyfill for getallheaders.", "support": { - "issues": "https://github.com/rlanvin/php-ip/issues", - "source": "https://github.com/rlanvin/php-ip/tree/master" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "time": "2015-06-26T06:50:37+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { "name": "roave/security-advisories", @@ -2984,68 +2404,6 @@ }, "time": "2023-06-12T17:37:14+00:00" }, - { - "name": "silinternational/simplesamlphp-module-silauth", - "version": "7.1.1", - "source": { - "type": "git", - "url": "https://github.com/silinternational/simplesamlphp-module-silauth.git", - "reference": "dbd85e48ffdca86ba1074941a08491fc73c29714" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silinternational/simplesamlphp-module-silauth/zipball/dbd85e48ffdca86ba1074941a08491fc73c29714", - "reference": "dbd85e48ffdca86ba1074941a08491fc73c29714", - "shasum": "" - }, - "require": { - "codemix/yii2-streamlog": "^1.3", - "ext-json": "*", - "fillup/fake-bower-assets": "^2.0", - "google/recaptcha": "^1.1", - "monolog/monolog": "^1.22", - "php": ">=7.0", - "psr/log": "^1.0", - "ramsey/uuid": "^3.5", - "rlanvin/php-ip": "^1.0", - "silinternational/idp-id-broker-php-client": "^4.0.0", - "silinternational/php-env": "^2.0 || ^3.0", - "silinternational/psr3-adapters": "^2.3 || ^3.0", - "silinternational/yii2-json-log-targets": "^2.0", - "simplesamlphp/simplesamlphp": "~1.18.6 || ~1.19.0", - "yiisoft/yii2": "~2.0.12", - "yiisoft/yii2-gii": "^2.0" - }, - "require-dev": { - "behat/behat": "^3.2", - "guzzlehttp/guzzle": "^6.0", - "phpunit/phpunit": "^8.0", - "roave/security-advisories": "dev-master" - }, - "type": "simplesamlphp-module", - "autoload": { - "psr-4": { - "Sil\\SilAuth\\": "src/", - "Sil\\SilAuth\\features\\": "features/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phillip Shipley", - "email": "phillip.shipley@gmail.com" - } - ], - "description": "SimpleSAMLphp auth module implementing various security measures before calls to IdP ID Broker backend", - "support": { - "issues": "https://github.com/silinternational/simplesamlphp-module-silauth/issues", - "source": "https://github.com/silinternational/simplesamlphp-module-silauth/tree/7.1.1" - }, - "time": "2023-07-31T22:19:59+00:00" - }, { "name": "silinternational/simplesamlphp-module-sildisco", "version": "4.0.0", @@ -3137,61 +2495,6 @@ }, "time": "2022-08-24T20:12:45+00:00" }, - { - "name": "silinternational/yii2-json-log-targets", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/silinternational/yii2-json-log-targets.git", - "reference": "6356b1cb3162c8f46ede7f246d19dd672f4d40f2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silinternational/yii2-json-log-targets/zipball/6356b1cb3162c8f46ede7f246d19dd672f4d40f2", - "reference": "6356b1cb3162c8f46ede7f246d19dd672f4d40f2", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "yiisoft/yii2": "^2.0" - }, - "require-dev": { - "behat/behat": "^3.3", - "codemix/yii2-streamlog": "^1.3", - "fillup/fake-bower-assets": "^2.0", - "phpunit/phpunit": "^8.0", - "roave/security-advisories": "dev-master", - "silinternational/email-service-php-client": "^2.0.0" - }, - "suggest": { - "codemix/yii2-streamlog": "To send log to a stream such as stdout using JsonStreamTarget", - "silinternational/email-service-php-client": "To send log via email using EmailServiceTarget" - }, - "type": "library", - "autoload": { - "psr-4": { - "Sil\\JsonLog\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A collection of Yii2 log targets that format the log message as a JSON string.", - "keywords": [ - "file", - "json", - "log", - "syslog", - "target", - "yii2" - ], - "support": { - "issues": "https://github.com/silinternational/yii2-json-log-targets/issues", - "source": "https://github.com/silinternational/yii2-json-log-targets/tree/2.1.0" - }, - "time": "2022-08-30T13:18:23+00:00" - }, { "name": "simplesamlphp/assert", "version": "v0.0.13", @@ -7502,291 +6805,6 @@ "source": "https://github.com/whitehat101/apr1-md5/tree/master" }, "time": "2015-02-11T11:06:42+00:00" - }, - { - "name": "yiisoft/yii2", - "version": "2.0.48.1", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "de92f154eefe322fc1b1b2a52cce46677441ced4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/de92f154eefe322fc1b1b2a52cce46677441ced4", - "reference": "de92f154eefe322fc1b1b2a52cce46677441ced4", - "shasum": "" - }, - "require": { - "bower-asset/inputmask": "~3.2.2 | ~3.3.5", - "bower-asset/jquery": "3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", - "bower-asset/punycode": "1.3.*", - "bower-asset/yii2-pjax": "~2.0.1", - "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", - "ext-ctype": "*", - "ext-mbstring": "*", - "ezyang/htmlpurifier": "^4.6", - "lib-pcre": "*", - "paragonie/random_compat": ">=1", - "php": ">=5.4.0", - "yiisoft/yii2-composer": "~2.0.4" - }, - "bin": [ - "yii" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com", - "homepage": "https://www.yiiframework.com/", - "role": "Founder and project lead" - }, - { - "name": "Alexander Makarov", - "email": "sam@rmcreative.ru", - "homepage": "https://rmcreative.ru/", - "role": "Core framework development" - }, - { - "name": "Maurizio Domba", - "homepage": "http://mdomba.info/", - "role": "Core framework development" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "https://www.cebe.cc/", - "role": "Core framework development" - }, - { - "name": "Timur Ruziev", - "email": "resurtm@gmail.com", - "homepage": "http://resurtm.com/", - "role": "Core framework development" - }, - { - "name": "Paul Klimov", - "email": "klimov.paul@gmail.com", - "role": "Core framework development" - }, - { - "name": "Dmitry Naumenko", - "email": "d.naumenko.a@gmail.com", - "role": "Core framework development" - }, - { - "name": "Boudewijn Vahrmeijer", - "email": "info@dynasource.eu", - "homepage": "http://dynasource.eu", - "role": "Core framework development" - } - ], - "description": "Yii PHP Framework Version 2", - "homepage": "https://www.yiiframework.com/", - "keywords": [ - "framework", - "yii2" - ], - "support": { - "forum": "https://forum.yiiframework.com/", - "irc": "ircs://irc.libera.chat:6697/yii", - "issues": "https://github.com/yiisoft/yii2/issues?state=open", - "source": "https://github.com/yiisoft/yii2", - "wiki": "https://www.yiiframework.com/wiki" - }, - "funding": [ - { - "url": "https://github.com/yiisoft", - "type": "github" - }, - { - "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2", - "type": "tidelift" - } - ], - "time": "2023-05-24T19:04:02+00:00" - }, - { - "name": "yiisoft/yii2-composer", - "version": "2.0.10", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/94bb3f66e779e2774f8776d6e1bdeab402940510", - "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 | ^2.0" - }, - "require-dev": { - "composer/composer": "^1.0 | ^2.0@dev", - "phpunit/phpunit": "<7" - }, - "type": "composer-plugin", - "extra": { - "class": "yii\\composer\\Plugin", - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\composer\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc" - } - ], - "description": "The composer plugin for Yii extension installer", - "keywords": [ - "composer", - "extension installer", - "yii2" - ], - "support": { - "forum": "http://www.yiiframework.com/forum/", - "irc": "irc://irc.freenode.net/yii", - "issues": "https://github.com/yiisoft/yii2-composer/issues", - "source": "https://github.com/yiisoft/yii2-composer", - "wiki": "http://www.yiiframework.com/wiki/" - }, - "funding": [ - { - "url": "https://github.com/yiisoft", - "type": "github" - }, - { - "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-composer", - "type": "tidelift" - } - ], - "time": "2020-06-24T00:04:01+00:00" - }, - { - "name": "yiisoft/yii2-gii", - "version": "2.2.6", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-gii.git", - "reference": "ac574e7e2c29fd865145c8688719f252d19aae23" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-gii/zipball/ac574e7e2c29fd865145c8688719f252d19aae23", - "reference": "ac574e7e2c29fd865145c8688719f252d19aae23", - "shasum": "" - }, - "require": { - "phpspec/php-diff": "^1.1.0", - "yiisoft/yii2": "~2.0.46" - }, - "require-dev": { - "cweagans/composer-patches": "^1.7", - "phpunit/phpunit": "4.8.34", - "yiisoft/yii2-coding-standards": "~2.0" - }, - "type": "yii2-extension", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - }, - "composer-exit-on-patch-failure": true, - "patches": { - "phpunit/phpunit-mock-objects": { - "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" - }, - "phpunit/php-file-iterator": { - "Fix PHP 8.1 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_path_file_iterator.patch" - }, - "phpunit/phpunit": { - "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", - "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch", - "Fix PHP 8.1 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php81.patch" - } - } - }, - "autoload": { - "psr-4": { - "yii\\gii\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com" - } - ], - "description": "The Gii extension for the Yii framework", - "keywords": [ - "code generator", - "gii", - "yii2" - ], - "support": { - "forum": "https://www.yiiframework.com/forum/", - "irc": "irc://irc.freenode.net/yii", - "issues": "https://github.com/yiisoft/yii2-gii/issues", - "source": "https://github.com/yiisoft/yii2-gii", - "wiki": "https://www.yiiframework.com/wiki/" - }, - "funding": [ - { - "url": "https://github.com/yiisoft", - "type": "github" - }, - { - "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-gii", - "type": "tidelift" - } - ], - "time": "2023-05-22T20:55:37+00:00" } ], "packages-dev": [ diff --git a/docker-compose.yml b/docker-compose.yml index 5edf404a..d42fc9ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth command: ["/data/run-debug.sh"] ports: @@ -72,6 +73,7 @@ services: - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth command: ["/data/run-tests.sh"] test-browser: @@ -120,6 +122,7 @@ services: - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth command: /data/run-debug.sh ports: - "80:80" @@ -167,6 +170,7 @@ services: - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' ports: - "8085:80" @@ -211,6 +215,7 @@ services: - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth command: /data/run.sh ports: - "8086:80" @@ -243,6 +248,7 @@ services: - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth ports: - "8081:80" environment: @@ -271,6 +277,7 @@ services: - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth ports: - "8082:80" environment: From 156be24753a9b451044bcd6358735c26e85a1927 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 14 May 2024 13:46:36 +0800 Subject: [PATCH 35/92] add composer dependencies for silauth module --- composer.json | 11 +- composer.lock | 822 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 831 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 131fb723..74cadbce 100644 --- a/composer.json +++ b/composer.json @@ -14,16 +14,25 @@ "ext-gmp": "*", "ext-json": "*", "ext-memcached": "*", + "codemix/yii2-streamlog": "^1.3", "simplesamlphp/simplesamlphp": "^1.19.6", "simplesamlphp/composer-module-installer": "1.1.8", + "rlanvin/php-ip": "^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", + "silinternational/yii2-json-log-targets": "^2.0", "gettext/gettext": "^4.8@dev", "silinternational/idp-id-broker-php-client": "^4.3", - "sinergi/browser-detector": "^6.1" + "sinergi/browser-detector": "^6.1", + "yiisoft/yii2": "~2.0.12", + "yiisoft/yii2-gii": "^2.0", + "fillup/fake-bower-assets": "^2.0", + "google/recaptcha": "^1.1", + "psr/log": "^1.0", + "monolog/monolog": "^1.22" }, "require-dev": { "behat/behat": "^3.8", diff --git a/composer.lock b/composer.lock index 08ce7361..bfda34c2 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": "8f7f00c703ea40df4fbdcf105af4b017", + "content-hash": "ae5996cffda6f7fd16dea5eb0e2a1665", "packages": [ { "name": "aws/aws-crt-php", @@ -155,6 +155,216 @@ }, "time": "2023-04-26T18:21:04+00:00" }, + { + "name": "cebe/markdown", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/cebe/markdown.git", + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86", + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86", + "shasum": "" + }, + "require": { + "lib-pcre": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "cebe/indent": "*", + "facebook/xhprof": "*@dev", + "phpunit/phpunit": "4.1.*" + }, + "bin": [ + "bin/markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "cebe\\markdown\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Creator" + } + ], + "description": "A super fast, highly extensible markdown parser for PHP", + "homepage": "https://github.com/cebe/markdown#readme", + "keywords": [ + "extensible", + "fast", + "gfm", + "markdown", + "markdown-extra" + ], + "support": { + "issues": "https://github.com/cebe/markdown/issues", + "source": "https://github.com/cebe/markdown" + }, + "time": "2018-03-26T11:24:36+00:00" + }, + { + "name": "codemix/yii2-streamlog", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/codemix/yii2-streamlog.git", + "reference": "1ed104f4bc4e961d1d08fb92d6854f01b1bd0b80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codemix/yii2-streamlog/zipball/1ed104f4bc4e961d1d08fb92d6854f01b1bd0b80", + "reference": "1ed104f4bc4e961d1d08fb92d6854f01b1bd0b80", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "codemix\\streamlog\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "A Yii 2 log target for streams in URL format", + "keywords": [ + "log", + "stdout", + "stream", + "yii2" + ], + "support": { + "issues": "https://github.com/codemix/yii2-streamlog/issues", + "source": "https://github.com/codemix/yii2-streamlog/tree/1.3.1" + }, + "time": "2020-03-26T13:23:28+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.17.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" + }, + "time": "2023-11-17T15:01:25+00:00" + }, + { + "name": "fillup/fake-bower-assets", + "version": "2.0.13", + "source": { + "type": "git", + "url": "https://github.com/fillup/fake-bower-assets.git", + "reference": "05ca2ac45757cee3906d6056c9623585c1eea68b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fillup/fake-bower-assets/zipball/05ca2ac45757cee3906d6056c9623585c1eea68b", + "reference": "05ca2ac45757cee3906d6056c9623585c1eea68b", + "shasum": "" + }, + "replace": { + "bower-asset/bootstrap": "3.3.6", + "bower-asset/inputmask": "3.2.2", + "bower-asset/jquery": "2.2.2", + "bower-asset/jquery.inputmask": "3.2.2", + "bower-asset/punycode": "1.3.1", + "bower-asset/typeahead.js": "0.11.1", + "bower-asset/yii2-pjax": "2.0.1", + "yiisoft/yii2-bootstrap": "2.0.6" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phillip Shipley", + "email": "phillip.shipley@gmail.com" + } + ], + "description": "Use Composer \"replace\" to fake out installing bower-asset dependencies that aren't really needed", + "support": { + "issues": "https://github.com/fillup/fake-bower-assets/issues", + "source": "https://github.com/fillup/fake-bower-assets/tree/master" + }, + "time": "2018-05-08T17:44:00+00:00" + }, { "name": "gettext/gettext", "version": "4.x-dev", @@ -310,6 +520,58 @@ ], "time": "2022-10-18T15:00:10+00:00" }, + { + "name": "google/recaptcha", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/google/recaptcha.git", + "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/google/recaptcha/zipball/d59a801e98a4e9174814a6d71bbc268dff1202df", + "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df", + "shasum": "" + }, + "require": { + "php": ">=8" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.14", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "ReCaptcha\\": "src/ReCaptcha" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.", + "homepage": "https://www.google.com/recaptcha/", + "keywords": [ + "Abuse", + "captcha", + "recaptcha", + "spam" + ], + "support": { + "forum": "https://groups.google.com/forum/#!forum/recaptcha", + "issues": "https://github.com/google/recaptcha/issues", + "source": "https://github.com/google/recaptcha" + }, + "time": "2023-02-18T17:41:46+00:00" + }, { "name": "guzzlehttp/command", "version": "1.3.0", @@ -956,6 +1218,92 @@ ], "time": "2022-01-13T18:05:33+00:00" }, + { + "name": "monolog/monolog", + "version": "1.27.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1", + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpstan/phpstan": "^0.12.59", + "phpunit/phpunit": "~4.5", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/1.27.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2022-06-09T08:53:42+00:00" + }, { "name": "mtdowling/jmespath.php", "version": "2.6.1", @@ -1017,6 +1365,56 @@ }, "time": "2021-06-14T00:11:39+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "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" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "phpfastcache/riak-client", "version": "3.4.3", @@ -1173,6 +1571,47 @@ ], "time": "2023-03-06T14:43:22+00:00" }, + { + "name": "phpspec/php-diff", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/php-diff.git", + "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/fc1156187f9f6c8395886fe85ed88a0a245d72e9", + "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Diff": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Boulton", + "homepage": "http://github.com/chrisboulton" + } + ], + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "support": { + "source": "https://github.com/phpspec/php-diff/tree/v1.1.3" + }, + "time": "2020-09-18T13:47:07+00:00" + }, { "name": "psr/cache", "version": "2.0.0", @@ -1524,6 +1963,47 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "rlanvin/php-ip", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/rlanvin/php-ip.git", + "reference": "03e507114b95e2d3a9b9621dc6fb583401afbf5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rlanvin/php-ip/zipball/03e507114b95e2d3a9b9621dc6fb583401afbf5e", + "reference": "03e507114b95e2d3a9b9621dc6fb583401afbf5e", + "shasum": "" + }, + "require": { + "ext-gmp": "*", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "IPv4/IPv6 manipulation library for PHP", + "homepage": "https://github.com/rlanvin/php-ip", + "keywords": [ + "IP", + "ipv4", + "ipv6" + ], + "support": { + "issues": "https://github.com/rlanvin/php-ip/issues", + "source": "https://github.com/rlanvin/php-ip/tree/master" + }, + "time": "2015-06-26T06:50:37+00:00" + }, { "name": "roave/security-advisories", "version": "dev-master", @@ -2495,6 +2975,61 @@ }, "time": "2022-08-24T20:12:45+00:00" }, + { + "name": "silinternational/yii2-json-log-targets", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/silinternational/yii2-json-log-targets.git", + "reference": "6356b1cb3162c8f46ede7f246d19dd672f4d40f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silinternational/yii2-json-log-targets/zipball/6356b1cb3162c8f46ede7f246d19dd672f4d40f2", + "reference": "6356b1cb3162c8f46ede7f246d19dd672f4d40f2", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "yiisoft/yii2": "^2.0" + }, + "require-dev": { + "behat/behat": "^3.3", + "codemix/yii2-streamlog": "^1.3", + "fillup/fake-bower-assets": "^2.0", + "phpunit/phpunit": "^8.0", + "roave/security-advisories": "dev-master", + "silinternational/email-service-php-client": "^2.0.0" + }, + "suggest": { + "codemix/yii2-streamlog": "To send log to a stream such as stdout using JsonStreamTarget", + "silinternational/email-service-php-client": "To send log via email using EmailServiceTarget" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sil\\JsonLog\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A collection of Yii2 log targets that format the log message as a JSON string.", + "keywords": [ + "file", + "json", + "log", + "syslog", + "target", + "yii2" + ], + "support": { + "issues": "https://github.com/silinternational/yii2-json-log-targets/issues", + "source": "https://github.com/silinternational/yii2-json-log-targets/tree/2.1.0" + }, + "time": "2022-08-30T13:18:23+00:00" + }, { "name": "simplesamlphp/assert", "version": "v0.0.13", @@ -6805,6 +7340,291 @@ "source": "https://github.com/whitehat101/apr1-md5/tree/master" }, "time": "2015-02-11T11:06:42+00:00" + }, + { + "name": "yiisoft/yii2", + "version": "2.0.49.3", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-framework.git", + "reference": "783f65c9a743dfd7484b6026f1aa6f25e37159d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/783f65c9a743dfd7484b6026f1aa6f25e37159d9", + "reference": "783f65c9a743dfd7484b6026f1aa6f25e37159d9", + "shasum": "" + }, + "require": { + "bower-asset/inputmask": "~3.2.2 | ~3.3.5", + "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", + "bower-asset/punycode": "1.3.*", + "bower-asset/yii2-pjax": "~2.0.1", + "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", + "ext-ctype": "*", + "ext-mbstring": "*", + "ezyang/htmlpurifier": "^4.6", + "lib-pcre": "*", + "paragonie/random_compat": ">=1", + "php": ">=5.4.0", + "yiisoft/yii2-composer": "~2.0.4" + }, + "bin": [ + "yii" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com", + "homepage": "https://www.yiiframework.com/", + "role": "Founder and project lead" + }, + { + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru", + "homepage": "https://rmcreative.ru/", + "role": "Core framework development" + }, + { + "name": "Maurizio Domba", + "homepage": "http://mdomba.info/", + "role": "Core framework development" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "https://www.cebe.cc/", + "role": "Core framework development" + }, + { + "name": "Timur Ruziev", + "email": "resurtm@gmail.com", + "homepage": "http://resurtm.com/", + "role": "Core framework development" + }, + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com", + "role": "Core framework development" + }, + { + "name": "Dmitry Naumenko", + "email": "d.naumenko.a@gmail.com", + "role": "Core framework development" + }, + { + "name": "Boudewijn Vahrmeijer", + "email": "info@dynasource.eu", + "homepage": "http://dynasource.eu", + "role": "Core framework development" + } + ], + "description": "Yii PHP Framework Version 2", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "framework", + "yii2" + ], + "support": { + "forum": "https://forum.yiiframework.com/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "source": "https://github.com/yiisoft/yii2", + "wiki": "https://www.yiiframework.com/wiki" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2", + "type": "tidelift" + } + ], + "time": "2023-10-31T15:39:08+00:00" + }, + { + "name": "yiisoft/yii2-composer", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-composer.git", + "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/94bb3f66e779e2774f8776d6e1bdeab402940510", + "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 | ^2.0" + }, + "require-dev": { + "composer/composer": "^1.0 | ^2.0@dev", + "phpunit/phpunit": "<7" + }, + "type": "composer-plugin", + "extra": { + "class": "yii\\composer\\Plugin", + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\composer\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc" + } + ], + "description": "The composer plugin for Yii extension installer", + "keywords": [ + "composer", + "extension installer", + "yii2" + ], + "support": { + "forum": "http://www.yiiframework.com/forum/", + "irc": "irc://irc.freenode.net/yii", + "issues": "https://github.com/yiisoft/yii2-composer/issues", + "source": "https://github.com/yiisoft/yii2-composer", + "wiki": "http://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-composer", + "type": "tidelift" + } + ], + "time": "2020-06-24T00:04:01+00:00" + }, + { + "name": "yiisoft/yii2-gii", + "version": "2.2.6", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-gii.git", + "reference": "ac574e7e2c29fd865145c8688719f252d19aae23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-gii/zipball/ac574e7e2c29fd865145c8688719f252d19aae23", + "reference": "ac574e7e2c29fd865145c8688719f252d19aae23", + "shasum": "" + }, + "require": { + "phpspec/php-diff": "^1.1.0", + "yiisoft/yii2": "~2.0.46" + }, + "require-dev": { + "cweagans/composer-patches": "^1.7", + "phpunit/phpunit": "4.8.34", + "yiisoft/yii2-coding-standards": "~2.0" + }, + "type": "yii2-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + }, + "composer-exit-on-patch-failure": true, + "patches": { + "phpunit/phpunit-mock-objects": { + "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" + }, + "phpunit/php-file-iterator": { + "Fix PHP 8.1 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_path_file_iterator.patch" + }, + "phpunit/phpunit": { + "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", + "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch", + "Fix PHP 8.1 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php81.patch" + } + } + }, + "autoload": { + "psr-4": { + "yii\\gii\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "description": "The Gii extension for the Yii framework", + "keywords": [ + "code generator", + "gii", + "yii2" + ], + "support": { + "forum": "https://www.yiiframework.com/forum/", + "irc": "irc://irc.freenode.net/yii", + "issues": "https://github.com/yiisoft/yii2-gii/issues", + "source": "https://github.com/yiisoft/yii2-gii", + "wiki": "https://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-gii", + "type": "tidelift" + } + ], + "time": "2023-05-22T20:55:37+00:00" } ], "packages-dev": [ From 618e29bad26bc393fab12f6dd0aae8de064001cf Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 14 May 2024 16:00:21 +0800 Subject: [PATCH 36/92] inherit from FeatureContext --- features/bootstrap/LoginContext.php | 3 +-- features/bootstrap/StatusContext.php | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/features/bootstrap/LoginContext.php b/features/bootstrap/LoginContext.php index 2e38ebd9..135fb937 100644 --- a/features/bootstrap/LoginContext.php +++ b/features/bootstrap/LoginContext.php @@ -1,7 +1,6 @@ Date: Tue, 14 May 2024 16:03:30 +0800 Subject: [PATCH 37/92] move silauth source files into the lib/Auth/Source path as recommended by SimpleSAMLphp documentation --- features/bootstrap/LoginContext.php | 29 +++++++++---------- features/bootstrap/StatusContext.php | 2 -- modules/silauth/lib/Auth/Source/SilAuth.php | 10 +++---- .../Auth/Source}/auth/AuthError.php | 2 +- .../Auth/Source}/auth/Authenticator.php | 18 ++++++------ .../Auth/Source}/auth/IdBroker.php | 4 +-- .../behaviors/CreatedAtUtcBehavior.php | 4 +-- .../Auth/Source}/captcha/Captcha.php | 4 +-- .../Auth/Source}/config/ConfigManager.php | 4 +-- .../Auth/Source}/config/ssp-config.php | 0 .../Auth/Source}/config/yii2-config.php | 4 +-- .../Auth/Source}/csrf/CsrfProtector.php | 2 +- .../{src => lib/Auth/Source}/http/Request.php | 4 +-- .../M161213135750CreateInitialTables.php | 2 +- .../M161213150831SwitchToUtcForDateTimes.php | 2 +- .../M170214141109CreateFailedLoginsTable.php | 2 +- .../M170214145629RemoveOldTables.php | 2 +- .../M170215141724SplitFailedLoginsTable.php | 2 +- .../Source}/models/FailedLoginIpAddress.php | 12 ++++---- .../models/FailedLoginIpAddressBase.php | 2 +- .../Source}/models/FailedLoginUsername.php | 10 +++---- .../models/FailedLoginUsernameBase.php | 2 +- .../Auth/Source}/rebuildbasemodels.sh | 2 +- .../{src => lib/Auth/Source}/saml/User.php | 2 +- .../Auth/Source}/system/System.php | 8 ++--- .../Auth/Source}/tests/bootstrap.php | 2 +- .../tests/fakes/FakeFailedIdBroker.php | 2 +- .../Auth/Source}/tests/fakes/FakeIdBroker.php | 6 ++-- .../tests/fakes/FakeInvalidIdBroker.php | 2 +- .../tests/fakes/FakeSuccessfulIdBroker.php | 2 +- .../Auth/Source}/tests/phpunit.xml | 0 .../tests/unit/auth/AuthenticatorTest.php | 6 ++-- .../tests/unit/captcha/DummyFailedCaptcha.php | 13 +++++++++ .../unit/captcha/DummySuccessfulCaptcha.php | 13 +++++++++ .../tests/unit/config/ConfigManagerTest.php | 4 +-- .../tests/unit/csrf/CsrfProtectorTest.php | 4 +-- .../Source}/tests/unit/csrf/FakeSession.php | 2 +- .../Source}/tests/unit/http/DummyRequest.php | 4 +-- .../Source}/tests/unit/http/RequestTest.php | 4 +-- .../unit/models/FailedLoginIpAddressTest.php | 8 ++--- .../unit/models/FailedLoginUsernameTest.php | 8 ++--- .../Auth/Source}/tests/unit/text/TextTest.php | 4 +-- .../Source}/tests/unit/time/UtcTimeTest.php | 4 +-- .../Source}/tests/unit/time/WaitTimeTest.php | 4 +-- .../{src => lib/Auth/Source}/text/Text.php | 2 +- .../{src => lib/Auth/Source}/time/UtcTime.php | 4 +-- .../Auth/Source}/time/WaitTime.php | 2 +- .../Auth/Source}/traits/LoggerAwareTrait.php | 2 +- modules/silauth/{src => lib/Auth/Source}/yii | 2 +- .../tests/unit/captcha/DummyFailedCaptcha.php | 13 --------- .../unit/captcha/DummySuccessfulCaptcha.php | 13 --------- modules/silauth/www/loginuserpass.php | 6 ++-- modules/silauth/www/status.php | 4 +-- 53 files changed, 136 insertions(+), 139 deletions(-) rename modules/silauth/{src => lib/Auth/Source}/auth/AuthError.php (97%) rename modules/silauth/{src => lib/Auth/Source}/auth/Authenticator.php (95%) rename modules/silauth/{src => lib/Auth/Source}/auth/IdBroker.php (96%) rename modules/silauth/{src => lib/Auth/Source}/behaviors/CreatedAtUtcBehavior.php (78%) rename modules/silauth/{src => lib/Auth/Source}/captcha/Captcha.php (85%) rename modules/silauth/{src => lib/Auth/Source}/config/ConfigManager.php (96%) rename modules/silauth/{src => lib/Auth/Source}/config/ssp-config.php (100%) rename modules/silauth/{src => lib/Auth/Source}/config/yii2-config.php (93%) rename modules/silauth/{src => lib/Auth/Source}/csrf/CsrfProtector.php (97%) rename modules/silauth/{src => lib/Auth/Source}/http/Request.php (98%) rename modules/silauth/{src => lib/Auth/Source}/migrations/M161213135750CreateInitialTables.php (97%) rename modules/silauth/{src => lib/Auth/Source}/migrations/M161213150831SwitchToUtcForDateTimes.php (91%) rename modules/silauth/{src => lib/Auth/Source}/migrations/M170214141109CreateFailedLoginsTable.php (95%) rename modules/silauth/{src => lib/Auth/Source}/migrations/M170214145629RemoveOldTables.php (92%) rename modules/silauth/{src => lib/Auth/Source}/migrations/M170215141724SplitFailedLoginsTable.php (95%) rename modules/silauth/{src => lib/Auth/Source}/models/FailedLoginIpAddress.php (92%) rename modules/silauth/{src => lib/Auth/Source}/models/FailedLoginIpAddressBase.php (94%) rename modules/silauth/{src => lib/Auth/Source}/models/FailedLoginUsername.php (92%) rename modules/silauth/{src => lib/Auth/Source}/models/FailedLoginUsernameBase.php (94%) rename modules/silauth/{src => lib/Auth/Source}/rebuildbasemodels.sh (85%) rename modules/silauth/{src => lib/Auth/Source}/saml/User.php (97%) rename modules/silauth/{src => lib/Auth/Source}/system/System.php (91%) rename modules/silauth/{src => lib/Auth/Source}/tests/bootstrap.php (83%) rename modules/silauth/{src => lib/Auth/Source}/tests/fakes/FakeFailedIdBroker.php (89%) rename modules/silauth/{src => lib/Auth/Source}/tests/fakes/FakeIdBroker.php (88%) rename modules/silauth/{src => lib/Auth/Source}/tests/fakes/FakeInvalidIdBroker.php (87%) rename modules/silauth/{src => lib/Auth/Source}/tests/fakes/FakeSuccessfulIdBroker.php (95%) rename modules/silauth/{src => lib/Auth/Source}/tests/phpunit.xml (100%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/auth/AuthenticatorTest.php (95%) create mode 100644 modules/silauth/lib/Auth/Source/tests/unit/captcha/DummyFailedCaptcha.php create mode 100644 modules/silauth/lib/Auth/Source/tests/unit/captcha/DummySuccessfulCaptcha.php rename modules/silauth/{src => lib/Auth/Source}/tests/unit/config/ConfigManagerTest.php (92%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/csrf/CsrfProtectorTest.php (85%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/csrf/FakeSession.php (93%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/http/DummyRequest.php (85%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/http/RequestTest.php (93%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/models/FailedLoginIpAddressTest.php (95%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/models/FailedLoginUsernameTest.php (95%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/text/TextTest.php (93%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/time/UtcTimeTest.php (97%) rename modules/silauth/{src => lib/Auth/Source}/tests/unit/time/WaitTimeTest.php (95%) rename modules/silauth/{src => lib/Auth/Source}/text/Text.php (94%) rename modules/silauth/{src => lib/Auth/Source}/time/UtcTime.php (96%) rename modules/silauth/{src => lib/Auth/Source}/time/WaitTime.php (97%) rename modules/silauth/{src => lib/Auth/Source}/traits/LoggerAwareTrait.php (90%) rename modules/silauth/{src => lib/Auth/Source}/yii (94%) delete mode 100644 modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php delete mode 100644 modules/silauth/src/tests/unit/captcha/DummySuccessfulCaptcha.php diff --git a/features/bootstrap/LoginContext.php b/features/bootstrap/LoginContext.php index 135fb937..61cc2e39 100644 --- a/features/bootstrap/LoginContext.php +++ b/features/bootstrap/LoginContext.php @@ -1,23 +1,22 @@ __DIR__ . '/../', 'id' => 'SilAuth', 'aliases' => [ - '@Sil/SilAuth' => __DIR__ . '/..', + '@SimpleSAML/Module/silauth/Auth/Source' => __DIR__ . '/..', ], 'bootstrap' => [ 'gii', @@ -57,7 +57,7 @@ 'migrate' => [ 'class' => 'yii\console\controllers\MigrateController', 'migrationNamespaces' => [ - 'Sil\\SilAuth\\migrations\\', + 'SimpleSAML\\Module\\silauth\\Auth\\Source\\migrations\\', ], // Disable non-namespaced migrations. diff --git a/modules/silauth/src/csrf/CsrfProtector.php b/modules/silauth/lib/Auth/Source/csrf/CsrfProtector.php similarity index 97% rename from modules/silauth/src/csrf/CsrfProtector.php rename to modules/silauth/lib/Auth/Source/csrf/CsrfProtector.php index 110575e4..b913d51f 100644 --- a/modules/silauth/src/csrf/CsrfProtector.php +++ b/modules/silauth/lib/Auth/Source/csrf/CsrfProtector.php @@ -1,5 +1,5 @@ ['db' => [ 'dsn' => sprintf( diff --git a/modules/silauth/src/tests/fakes/FakeFailedIdBroker.php b/modules/silauth/lib/Auth/Source/tests/fakes/FakeFailedIdBroker.php similarity index 89% rename from modules/silauth/src/tests/fakes/FakeFailedIdBroker.php rename to modules/silauth/lib/Auth/Source/tests/fakes/FakeFailedIdBroker.php index 776c4a4c..a2a04fc7 100644 --- a/modules/silauth/src/tests/fakes/FakeFailedIdBroker.php +++ b/modules/silauth/lib/Auth/Source/tests/fakes/FakeFailedIdBroker.php @@ -1,5 +1,5 @@ ['db' => [ 'dsn' => sprintf( diff --git a/modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php b/modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php deleted file mode 100644 index 3d18f2c6..00000000 --- a/modules/silauth/src/tests/unit/captcha/DummyFailedCaptcha.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Tue, 14 May 2024 16:03:54 +0800 Subject: [PATCH 38/92] define BASE_URL_PATH as required by silauth --- actions-services.yml | 1 + docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/actions-services.yml b/actions-services.yml index 817ccb20..98e84210 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -111,6 +111,7 @@ services: SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" THEME_USE: "default" + BASE_URL_PATH: "http://ssp-idp1.local/" ssp-idp2.local: build: . diff --git a/docker-compose.yml b/docker-compose.yml index d42fc9ed..d4384eb4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -196,6 +196,7 @@ services: MYSQL_DATABASE: "silauth" MYSQL_USER: "silauth" MYSQL_PASSWORD: "silauth" + BASE_URL_PATH: "http://ssp-idp1.local/" ssp-idp2.local: build: . From c1856776c5c04286c86f96ecd8998cb5b104d9b4 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 14 May 2024 16:18:21 +0800 Subject: [PATCH 39/92] update StatusContext with new IDP URL --- features/bootstrap/StatusContext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/bootstrap/StatusContext.php b/features/bootstrap/StatusContext.php index 52f3e26a..a78c213a 100644 --- a/features/bootstrap/StatusContext.php +++ b/features/bootstrap/StatusContext.php @@ -16,7 +16,7 @@ class StatusContext extends FeatureContext public function iCheckTheStatusOfThisModule() { $client = new Client(); - $response = $client->get('http://testweb/module.php/silauth/status.php'); + $response = $client->get('http://ssp-idp1.local/module.php/silauth/status.php'); $this->responseCode = $response->getStatusCode(); $this->responseText = $response->getBody()->getContents(); } @@ -39,7 +39,7 @@ public function iRequestTheInitialLoginPageOfThisModule() 'cookies' => true, 'http_errors' => false, ]); - $response = $client->get('http://testweb/module.php/core/authenticate.php?as=silauth'); + $response = $client->get('http://ssp-idp1.local/module.php/core/authenticate.php?as=silauth'); $this->responseCode = $response->getStatusCode(); } From 3ffdf5dc050584690696bddadb6cef4d79649d4b Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 14 May 2024 16:31:30 +0800 Subject: [PATCH 40/92] run database migrations on idp1 --- actions-services.yml | 5 ++++- docker-compose.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/actions-services.yml b/actions-services.yml index 98e84210..fccc1410 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -93,7 +93,10 @@ services: # Include the features folder (for the FakeIdBrokerClient class) - ./features:/data/features - command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' + command: > + bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && + /data/enable-exampleauth-module.sh && + /data/run.sh" environment: ADMIN_EMAIL: "john_doe@there.com" ADMIN_PASS: "a" diff --git a/docker-compose.yml b/docker-compose.yml index d4384eb4..194bc100 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -171,7 +171,10 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - command: 'bash -c "/data/enable-exampleauth-module.sh && /data/run.sh"' + command: > + bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && + /data/enable-exampleauth-module.sh && + /data/run.sh" ports: - "8085:80" environment: From 06732ca774df50afec6a6197b0c55149ef2bf014 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 14 May 2024 16:39:18 +0800 Subject: [PATCH 41/92] add database params to the test container also --- actions-services.yml | 4 ++++ docker-compose.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/actions-services.yml b/actions-services.yml index fccc1410..b619b49d 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -25,6 +25,10 @@ services: - ADMIN_PASS=b - SECRET_SALT=abc123 - IDP_NAME=x + - MYSQL_HOST=db + - MYSQL_DATABASE=silauth + - MYSQL_USER=silauth + - MYSQL_PASSWORD=silauth volumes: - ./dockerbuild/run-integration-tests.sh:/data/run-integration-tests.sh - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh diff --git a/docker-compose.yml b/docker-compose.yml index 194bc100..39e10f9c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,6 +53,10 @@ services: - pwmanager.local - test-browser environment: + - MYSQL_HOST=db + - MYSQL_DATABASE=silauth + - MYSQL_USER=silauth + - MYSQL_PASSWORD=silauth - COMPOSER_CACHE_DIR=/composer - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com From d2d0837bf6f449ed1a717a47bd533a54dac955f1 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 14 May 2024 16:46:52 +0800 Subject: [PATCH 42/92] increase whenavail timouts --- dockerbuild/run-integration-tests.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dockerbuild/run-integration-tests.sh b/dockerbuild/run-integration-tests.sh index 71d8c006..51d8fa04 100755 --- a/dockerbuild/run-integration-tests.sh +++ b/dockerbuild/run-integration-tests.sh @@ -6,9 +6,9 @@ set -x cd /data export COMPOSER_ALLOW_SUPERUSER=1; composer install -whenavail "ssp-hub.local" 80 15 echo Hub ready -whenavail "ssp-idp1.local" 80 5 echo IDP 1 ready -whenavail "ssp-sp1.local" 80 5 echo SP 1 ready +whenavail "ssp-hub.local" 80 15 echo Hub ready +whenavail "ssp-idp1.local" 80 15 echo IDP 1 ready +whenavail "ssp-sp1.local" 80 15 echo SP 1 ready ./vendor/bin/behat \ --no-interaction \ From 2aafb0668270377f63a8cac38212dae030eca59b Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 14 May 2024 17:13:32 +0800 Subject: [PATCH 43/92] fix actions-services.yml --- actions-services.yml | 14 ++++++++++---- dockerbuild/run-integration-tests.sh | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/actions-services.yml b/actions-services.yml index b619b49d..756fd699 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -20,15 +20,15 @@ services: - pwmanager.local - test-browser environment: + - MYSQL_HOST=db + - MYSQL_DATABASE=silauth + - MYSQL_USER=silauth + - MYSQL_PASSWORD=silauth - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=b - SECRET_SALT=abc123 - IDP_NAME=x - - MYSQL_HOST=db - - MYSQL_DATABASE=silauth - - MYSQL_USER=silauth - - MYSQL_PASSWORD=silauth volumes: - ./dockerbuild/run-integration-tests.sh:/data/run-integration-tests.sh - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -74,6 +74,8 @@ services: ssp-idp1.local: build: . + depends_on: + - db volumes: # Utilize custom certs - ./development/idp-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert @@ -118,6 +120,10 @@ services: SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" THEME_USE: "default" + MYSQL_HOST: "db" + MYSQL_DATABASE: "silauth" + MYSQL_USER: "silauth" + MYSQL_PASSWORD: "silauth" BASE_URL_PATH: "http://ssp-idp1.local/" ssp-idp2.local: diff --git a/dockerbuild/run-integration-tests.sh b/dockerbuild/run-integration-tests.sh index 51d8fa04..f4ca4aa3 100755 --- a/dockerbuild/run-integration-tests.sh +++ b/dockerbuild/run-integration-tests.sh @@ -7,8 +7,8 @@ cd /data export COMPOSER_ALLOW_SUPERUSER=1; composer install whenavail "ssp-hub.local" 80 15 echo Hub ready -whenavail "ssp-idp1.local" 80 15 echo IDP 1 ready -whenavail "ssp-sp1.local" 80 15 echo SP 1 ready +whenavail "ssp-idp1.local" 80 5 echo IDP 1 ready +whenavail "ssp-sp1.local" 80 5 echo SP 1 ready ./vendor/bin/behat \ --no-interaction \ From a7c63a7410340df6336b7b4ae5f93d46fd4056f1 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 13:16:08 +0800 Subject: [PATCH 44/92] add Profile Review README details [skip ci] --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 4986cd25..8aaebb94 100644 --- a/README.md +++ b/README.md @@ -171,3 +171,52 @@ can be autoloaded, to use as the logger within ExpiryDate. This is adapted from the `ssp-iidp-expirycheck` and `expirycheck` modules. Thanks to Alex Mihičinac, Steve Moitozo, and Steve Bagwell for the initial work they did on those two modules. + +### Profile Review SimpleSAMLphp Module + +A simpleSAMLphp module for prompting the user review their profile (such as +2-step verification, email, etc.). + +This module is implemented as an Authentication Processing Filter, +or AuthProc. That means it can be configured in the global config.php file or +the SP remote or IdP hosted metadata. + +It is recommended to run the profilereview module at the IdP, after all +other authentication modules. + +#### How to use the module + +You will need to set filter parameters in your config. We recommend adding +them to the `'authproc'` array in your `metadata/saml20-idp-hosted.php` file. + +Example (for `metadata/saml20-idp-hosted.php`): + + use Sil\PhpEnv\Env; + use Sil\Psr3Adapters\Psr3SamlLogger; + + // ... + + 'authproc' => [ + 10 => [ + // Required: + 'class' => 'profilereview:ProfileReview', + 'employeeIdAttr' => 'employeeNumber', + 'profileUrl' => Env::get('PROFILE_URL'), + 'mfaLearnMoreUrl' => Env::get('MFA_LEARN_MORE_URL'), + + // Optional: + 'loggerClass' => Psr3SamlLogger::class, + ], + + // ... + ], + +The `employeeIdAttr` parameter represents the SAML attribute name which has +the user's Employee ID stored in it. In certain situations, this may be +displayed to the user, as well as being used in log messages. + +The `loggerClass` parameter specifies the name of a PSR-3 compatible class that +can be autoloaded, to use as the logger within ExpiryDate. + +The `profileUrl` parameter is for the URL of where to send the user if they +want/need to update their profile. From a8939e5c8f9152fb261c7ae238a4092c24731f36 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 15:32:20 +0800 Subject: [PATCH 45/92] changed namespace on FakeIdBrokerClient to match the repo name and path --- composer.json | 2 +- development/idp-local/config/authsources.php | 2 +- development/idp-local/metadata/saml20-idp-hosted.php | 2 +- features/bootstrap/MfaContext.php | 2 +- features/fakes/FakeIdBrokerClient.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index b5a9031d..a28cafb2 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "vendor/yiisoft/yii2/Yii.php" ], "psr-4": { - "Sil\\SspMfa\\Behat\\": "features/" + "SilInternational\\SspBase\\Features\\": "features/" } }, "config": { diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 3af3dac7..99069ecd 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -1,6 +1,6 @@ Date: Wed, 15 May 2024 17:09:34 +0800 Subject: [PATCH 46/92] comments to identify which users are for which module's tests [skip ci] --- development/idp-local/config/authsources.php | 72 ++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 99069ecd..54cadf20 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -15,6 +15,8 @@ // Set up example users for testing expirychecker module. 'example-userpass' => [ 'exampleauth:UserPass', + + // expirychecker test user whose password expires in the distant future 'distant_future:a' => [ 'eduPersonPrincipalName' => ['DISTANT_FUTURE@ssp-idp1.local'], 'sn' => ['Future'], @@ -29,6 +31,8 @@ gmdate('YmdHis\Z', strtotime('+6 months')), // Distant future ], ], + + // expirychecker test user whose password expires in the near future 'near_future:b' => [ 'eduPersonPrincipalName' => ['NEAR_FUTURE@ssp-idp1.local'], 'sn' => ['Future'], @@ -43,6 +47,8 @@ gmdate('YmdHis\Z', strtotime('+1 day')), // Very soon ], ], + + // expirychecker test user whose password expires in the past 'already_past:c' => [ 'eduPersonPrincipalName' => ['ALREADY_PAST@ssp-idp1.local'], 'sn' => ['Past'], @@ -57,6 +63,8 @@ gmdate('YmdHis\Z', strtotime('-1 day')), // In the past ], ], + + // expirychecker test user whose password expiry is missing 'missing_exp:d' => [ 'eduPersonPrincipalName' => ['MISSING_EXP@ssp-idp-1.local'], 'sn' => ['Expiration'], @@ -65,6 +73,8 @@ 'employeeNumber' => ['44444'], 'cn' => ['MISSING_EXP'], ], + + // expirychecker test user whose password expiry is invalid 'invalid_exp:e' => [ 'eduPersonPrincipalName' => ['INVALID_EXP@ssp-idp-1.local'], 'sn' => ['Expiration'], @@ -79,6 +89,8 @@ 'invalid' ], ], + + // profilereview test user whose profile is not due for review 'no_review:e' => [ 'eduPersonPrincipalName' => ['NO_REVIEW@idp'], 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], @@ -111,6 +123,8 @@ ], 'profile_review' => 'no' ], + + // profilereview test user whose profile is flagged for mfa_add review 'mfa_add:f' => [ 'eduPersonPrincipalName' => ['MFA_ADD@idp'], 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], @@ -132,6 +146,8 @@ ], 'profile_review' => 'no' ], + + // profilereview test user whose profile is flagged for method_add review 'method_add:g' => [ 'eduPersonPrincipalName' => ['METHOD_ADD@methodidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -164,6 +180,8 @@ ], 'profile_review' => 'no' ], + + // profilereview test user whose profile is flagged for profile review 'profile_review:h' => [ 'eduPersonPrincipalName' => ['METHOD_REVIEW@methodidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -213,6 +231,8 @@ ], 'profile_review' => 'yes' ], + + // mfa test user who does not require mfa 'no_mfa_needed:a' => [ 'eduPersonPrincipalName' => ['NO_MFA_NEEDED@mfaidp'], 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], @@ -235,6 +255,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa to be set up 'must_set_up_mfa:a' => [ 'eduPersonPrincipalName' => ['MUST_SET_UP_MFA@mfaidp'], 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], @@ -257,6 +279,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes 'has_backupcode:a' => [ 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], @@ -287,6 +311,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes and a manager email 'has_backupcode_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], @@ -318,6 +344,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has totp 'has_totp:a' => [ 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -346,6 +374,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and a manager email 'has_totp_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -375,6 +405,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has a webauthn 'has_webauthn:a' => [ 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -408,6 +440,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has webauthn and a manager email 'has_webauthn_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -437,6 +471,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has all forms of mfa 'has_all:a' => [ 'eduPersonPrincipalName' => ['has_all@mfaidp'], 'eduPersonTargetID' => ['77777777-7777-7777-7777-777777777777'], @@ -478,6 +514,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who has a rate-limited mfa 'has_rate_limited_mfa:a' => [ 'eduPersonPrincipalName' => ['HAS_RATE_LIMITED_MFA@mfaidp'], 'eduPersonTargetID' => ['88888888-8888-8888-8888-888888888888'], @@ -508,6 +546,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has 4 backup codes 'has_4_backupcodes:a' => [ 'eduPersonPrincipalName' => ['HAS_4_BACKUPCODES@mfaidp'], 'eduPersonTargetID' => ['99999999-9999-9999-9999-999999999999'], @@ -538,6 +578,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has 1 backup code remaining 'has_1_backupcode_only:a' => [ 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_ONLY@mfaidp'], 'eduPersonTargetID' => ['00000010-0010-0010-0010-000000000010'], @@ -568,6 +610,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has one backup code plus another option 'has_1_backupcode_plus:a' => [ 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_PLUS@mfaidp'], 'eduPersonTargetID' => ['00000011-0011-0011-0011-000000000011'], @@ -603,6 +647,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has webauthn and totp 'has_webauthn_totp:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], @@ -636,6 +682,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has webauthn, totp and a manager email 'has_webauthn_totp_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], @@ -670,6 +718,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has webauthn and backup codes 'has_webauthn_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], @@ -705,6 +755,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes and a manager email 'has_webauthn_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], @@ -741,6 +793,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has totp and backup codes 'has_webauthn_totp_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], @@ -781,6 +835,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes, totp, and a manager email 'has_webauthn_totp_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], @@ -822,6 +878,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has manager code, webauthn, and a more-recently used totp 'has_mgr_code_webauthn_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_mgr_code_webauthn_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000114-0014-0014-0014-000000000014'], @@ -863,6 +921,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has webauthn and more recently used totp 'has_webauthn_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000214-0014-0014-0014-000000000014'], @@ -898,6 +958,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and more recently used webauthn 'has_totp_and_more_recently_used_webauthn:a' => [ 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_webauthn@mfaidp'], 'eduPersonTargetID' => ['00000314-0014-0014-0014-000000000014'], @@ -933,6 +995,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and more recently-used backup code 'has_totp_and_more_recently_used_backup_code:a' => [ 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_backup_code@mfaidp'], 'eduPersonTargetID' => ['00000414-0014-0014-0014-000000000014'], @@ -970,6 +1034,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup code and a more recently used totp 'has_backup_code_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_backup_code_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000514-0014-0014-0014-000000000014'], @@ -1007,6 +1073,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and backup codes 'has_totp_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], @@ -1042,6 +1110,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp, backup codes, and manager email 'has_totp_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], @@ -1078,6 +1148,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has backup codes and manager code 'has_mgr_code:a' => [ 'eduPersonPrincipalName' => ['has_mgr_code@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], From 119cd493ad950d0d46817659ac5d1d7a58f20106 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:11:12 +0800 Subject: [PATCH 47/92] add comments to describe the reason for disabled test cases [skip ci] --- features/mfa.feature | 3 +++ 1 file changed, 3 insertions(+) diff --git a/features/mfa.feature b/features/mfa.feature index a61a61ab..3a0f477c 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -141,6 +141,7 @@ Feature: Prompt for MFA credentials | | TOTP | | supports WebAuthn | TOTP | | | TOTP | , backup codes | supports WebAuthn | TOTP | | | | backup codes | supports WebAuthn | backup code | +# The following cases are disabled due to lack of test support for changing web client user agent # | WebAuthn | | | does not support WebAuthn | WebAuthn | # | WebAuthn | , TOTP | | does not support WebAuthn | TOTP | # | WebAuthn | | , backup codes | does not support WebAuthn | backup code | @@ -163,6 +164,7 @@ Feature: Prompt for MFA credentials | TOTP | WebAuthn | supports WebAuthn | WebAuthn | | TOTP | backup code | supports WebAuthn | backup code | | backup code | TOTP | supports WebAuthn | TOTP | +# The following case is disabled due to lack of test support for changing web client user agent # | TOTP | WebAuthn | does not support WebAuthn | TOTP | Scenario: Defaulting to the manager code despite having a used mfa @@ -180,6 +182,7 @@ Feature: Prompt for MFA credentials Examples: | supports WebAuthn or not | should or not | | supports WebAuthn | should not | +# The following case is disabled due to lack of test support for changing web client user agent # | does not support WebAuthn | should | Scenario Outline: When to show the link to send a manager rescue code From 985626671457ba21c9d0213bad75e3d83580b504 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:13:01 +0800 Subject: [PATCH 48/92] add comment to explain why a profile review is required --- features/mfa.feature | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/mfa.feature b/features/mfa.feature index 3a0f477c..1ccdba3a 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -219,6 +219,7 @@ Feature: Prompt for MFA credentials Given I provide credentials that have a manager code And I login When I submit the correct manager code + # because profile review is required after using a manager code: And I click the remind-me-later button Then I should end up at my intended destination @@ -229,6 +230,7 @@ Feature: Prompt for MFA credentials And I click the Request Assistance link And I click the Send a code link When I submit the correct manager code + # because profile review is required after using a manager code: And I click the remind-me-later button Then I should end up at my intended destination From d58e413dc4a40617c70406250ffd115e80fcc6cf Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:09:34 +0800 Subject: [PATCH 49/92] comments to identify which users are for which module's tests [skip ci] --- development/idp-local/config/authsources.php | 72 ++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 99069ecd..54cadf20 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -15,6 +15,8 @@ // Set up example users for testing expirychecker module. 'example-userpass' => [ 'exampleauth:UserPass', + + // expirychecker test user whose password expires in the distant future 'distant_future:a' => [ 'eduPersonPrincipalName' => ['DISTANT_FUTURE@ssp-idp1.local'], 'sn' => ['Future'], @@ -29,6 +31,8 @@ gmdate('YmdHis\Z', strtotime('+6 months')), // Distant future ], ], + + // expirychecker test user whose password expires in the near future 'near_future:b' => [ 'eduPersonPrincipalName' => ['NEAR_FUTURE@ssp-idp1.local'], 'sn' => ['Future'], @@ -43,6 +47,8 @@ gmdate('YmdHis\Z', strtotime('+1 day')), // Very soon ], ], + + // expirychecker test user whose password expires in the past 'already_past:c' => [ 'eduPersonPrincipalName' => ['ALREADY_PAST@ssp-idp1.local'], 'sn' => ['Past'], @@ -57,6 +63,8 @@ gmdate('YmdHis\Z', strtotime('-1 day')), // In the past ], ], + + // expirychecker test user whose password expiry is missing 'missing_exp:d' => [ 'eduPersonPrincipalName' => ['MISSING_EXP@ssp-idp-1.local'], 'sn' => ['Expiration'], @@ -65,6 +73,8 @@ 'employeeNumber' => ['44444'], 'cn' => ['MISSING_EXP'], ], + + // expirychecker test user whose password expiry is invalid 'invalid_exp:e' => [ 'eduPersonPrincipalName' => ['INVALID_EXP@ssp-idp-1.local'], 'sn' => ['Expiration'], @@ -79,6 +89,8 @@ 'invalid' ], ], + + // profilereview test user whose profile is not due for review 'no_review:e' => [ 'eduPersonPrincipalName' => ['NO_REVIEW@idp'], 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], @@ -111,6 +123,8 @@ ], 'profile_review' => 'no' ], + + // profilereview test user whose profile is flagged for mfa_add review 'mfa_add:f' => [ 'eduPersonPrincipalName' => ['MFA_ADD@idp'], 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], @@ -132,6 +146,8 @@ ], 'profile_review' => 'no' ], + + // profilereview test user whose profile is flagged for method_add review 'method_add:g' => [ 'eduPersonPrincipalName' => ['METHOD_ADD@methodidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -164,6 +180,8 @@ ], 'profile_review' => 'no' ], + + // profilereview test user whose profile is flagged for profile review 'profile_review:h' => [ 'eduPersonPrincipalName' => ['METHOD_REVIEW@methodidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -213,6 +231,8 @@ ], 'profile_review' => 'yes' ], + + // mfa test user who does not require mfa 'no_mfa_needed:a' => [ 'eduPersonPrincipalName' => ['NO_MFA_NEEDED@mfaidp'], 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], @@ -235,6 +255,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa to be set up 'must_set_up_mfa:a' => [ 'eduPersonPrincipalName' => ['MUST_SET_UP_MFA@mfaidp'], 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], @@ -257,6 +279,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes 'has_backupcode:a' => [ 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], @@ -287,6 +311,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes and a manager email 'has_backupcode_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], @@ -318,6 +344,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has totp 'has_totp:a' => [ 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -346,6 +374,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and a manager email 'has_totp_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -375,6 +405,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has a webauthn 'has_webauthn:a' => [ 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -408,6 +440,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has webauthn and a manager email 'has_webauthn_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -437,6 +471,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has all forms of mfa 'has_all:a' => [ 'eduPersonPrincipalName' => ['has_all@mfaidp'], 'eduPersonTargetID' => ['77777777-7777-7777-7777-777777777777'], @@ -478,6 +514,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who has a rate-limited mfa 'has_rate_limited_mfa:a' => [ 'eduPersonPrincipalName' => ['HAS_RATE_LIMITED_MFA@mfaidp'], 'eduPersonTargetID' => ['88888888-8888-8888-8888-888888888888'], @@ -508,6 +546,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has 4 backup codes 'has_4_backupcodes:a' => [ 'eduPersonPrincipalName' => ['HAS_4_BACKUPCODES@mfaidp'], 'eduPersonTargetID' => ['99999999-9999-9999-9999-999999999999'], @@ -538,6 +578,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has 1 backup code remaining 'has_1_backupcode_only:a' => [ 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_ONLY@mfaidp'], 'eduPersonTargetID' => ['00000010-0010-0010-0010-000000000010'], @@ -568,6 +610,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has one backup code plus another option 'has_1_backupcode_plus:a' => [ 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_PLUS@mfaidp'], 'eduPersonTargetID' => ['00000011-0011-0011-0011-000000000011'], @@ -603,6 +647,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has webauthn and totp 'has_webauthn_totp:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], @@ -636,6 +682,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has webauthn, totp and a manager email 'has_webauthn_totp_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], @@ -670,6 +718,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has webauthn and backup codes 'has_webauthn_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], @@ -705,6 +755,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes and a manager email 'has_webauthn_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], @@ -741,6 +793,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has totp and backup codes 'has_webauthn_totp_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], @@ -781,6 +835,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup codes, totp, and a manager email 'has_webauthn_totp_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], @@ -822,6 +878,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has manager code, webauthn, and a more-recently used totp 'has_mgr_code_webauthn_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_mgr_code_webauthn_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000114-0014-0014-0014-000000000014'], @@ -863,6 +921,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has webauthn and more recently used totp 'has_webauthn_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000214-0014-0014-0014-000000000014'], @@ -898,6 +958,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and more recently used webauthn 'has_totp_and_more_recently_used_webauthn:a' => [ 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_webauthn@mfaidp'], 'eduPersonTargetID' => ['00000314-0014-0014-0014-000000000014'], @@ -933,6 +995,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and more recently-used backup code 'has_totp_and_more_recently_used_backup_code:a' => [ 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_backup_code@mfaidp'], 'eduPersonTargetID' => ['00000414-0014-0014-0014-000000000014'], @@ -970,6 +1034,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has backup code and a more recently used totp 'has_backup_code_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_backup_code_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000514-0014-0014-0014-000000000014'], @@ -1007,6 +1073,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp and backup codes 'has_totp_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], @@ -1042,6 +1110,8 @@ 'options' => [], ], ], + + // mfa test user who requires mfa and has totp, backup codes, and manager email 'has_totp_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], @@ -1078,6 +1148,8 @@ ], 'manager_email' => ['manager@example.com'], ], + + // mfa test user who requires mfa and has backup codes and manager code 'has_mgr_code:a' => [ 'eduPersonPrincipalName' => ['has_mgr_code@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], From 25f01d1018588ff2630e9b665aab53e0be85528a Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:11:12 +0800 Subject: [PATCH 50/92] add comments to describe the reason for disabled test cases [skip ci] --- features/mfa.feature | 3 +++ 1 file changed, 3 insertions(+) diff --git a/features/mfa.feature b/features/mfa.feature index a61a61ab..3a0f477c 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -141,6 +141,7 @@ Feature: Prompt for MFA credentials | | TOTP | | supports WebAuthn | TOTP | | | TOTP | , backup codes | supports WebAuthn | TOTP | | | | backup codes | supports WebAuthn | backup code | +# The following cases are disabled due to lack of test support for changing web client user agent # | WebAuthn | | | does not support WebAuthn | WebAuthn | # | WebAuthn | , TOTP | | does not support WebAuthn | TOTP | # | WebAuthn | | , backup codes | does not support WebAuthn | backup code | @@ -163,6 +164,7 @@ Feature: Prompt for MFA credentials | TOTP | WebAuthn | supports WebAuthn | WebAuthn | | TOTP | backup code | supports WebAuthn | backup code | | backup code | TOTP | supports WebAuthn | TOTP | +# The following case is disabled due to lack of test support for changing web client user agent # | TOTP | WebAuthn | does not support WebAuthn | TOTP | Scenario: Defaulting to the manager code despite having a used mfa @@ -180,6 +182,7 @@ Feature: Prompt for MFA credentials Examples: | supports WebAuthn or not | should or not | | supports WebAuthn | should not | +# The following case is disabled due to lack of test support for changing web client user agent # | does not support WebAuthn | should | Scenario Outline: When to show the link to send a manager rescue code From 41e3bfbb6e3c20396b17fa2fc0453ef14ec27793 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:13:01 +0800 Subject: [PATCH 51/92] add comment to explain why a profile review is required --- features/mfa.feature | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/mfa.feature b/features/mfa.feature index 3a0f477c..1ccdba3a 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -219,6 +219,7 @@ Feature: Prompt for MFA credentials Given I provide credentials that have a manager code And I login When I submit the correct manager code + # because profile review is required after using a manager code: And I click the remind-me-later button Then I should end up at my intended destination @@ -229,6 +230,7 @@ Feature: Prompt for MFA credentials And I click the Request Assistance link And I click the Send a code link When I submit the correct manager code + # because profile review is required after using a manager code: And I click the remind-me-later button Then I should end up at my intended destination From c4afaf1b4daf44d0c7cb9acb7b4458677bbf7cc2 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:18:57 +0800 Subject: [PATCH 52/92] Revert "comments to identify which users are for which module's tests [skip ci]" This reverts commit 5634a0b944eabe8d52d0e61257aec6fe1408d78b. --- development/idp-local/config/authsources.php | 72 -------------------- 1 file changed, 72 deletions(-) diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 54cadf20..99069ecd 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -15,8 +15,6 @@ // Set up example users for testing expirychecker module. 'example-userpass' => [ 'exampleauth:UserPass', - - // expirychecker test user whose password expires in the distant future 'distant_future:a' => [ 'eduPersonPrincipalName' => ['DISTANT_FUTURE@ssp-idp1.local'], 'sn' => ['Future'], @@ -31,8 +29,6 @@ gmdate('YmdHis\Z', strtotime('+6 months')), // Distant future ], ], - - // expirychecker test user whose password expires in the near future 'near_future:b' => [ 'eduPersonPrincipalName' => ['NEAR_FUTURE@ssp-idp1.local'], 'sn' => ['Future'], @@ -47,8 +43,6 @@ gmdate('YmdHis\Z', strtotime('+1 day')), // Very soon ], ], - - // expirychecker test user whose password expires in the past 'already_past:c' => [ 'eduPersonPrincipalName' => ['ALREADY_PAST@ssp-idp1.local'], 'sn' => ['Past'], @@ -63,8 +57,6 @@ gmdate('YmdHis\Z', strtotime('-1 day')), // In the past ], ], - - // expirychecker test user whose password expiry is missing 'missing_exp:d' => [ 'eduPersonPrincipalName' => ['MISSING_EXP@ssp-idp-1.local'], 'sn' => ['Expiration'], @@ -73,8 +65,6 @@ 'employeeNumber' => ['44444'], 'cn' => ['MISSING_EXP'], ], - - // expirychecker test user whose password expiry is invalid 'invalid_exp:e' => [ 'eduPersonPrincipalName' => ['INVALID_EXP@ssp-idp-1.local'], 'sn' => ['Expiration'], @@ -89,8 +79,6 @@ 'invalid' ], ], - - // profilereview test user whose profile is not due for review 'no_review:e' => [ 'eduPersonPrincipalName' => ['NO_REVIEW@idp'], 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], @@ -123,8 +111,6 @@ ], 'profile_review' => 'no' ], - - // profilereview test user whose profile is flagged for mfa_add review 'mfa_add:f' => [ 'eduPersonPrincipalName' => ['MFA_ADD@idp'], 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], @@ -146,8 +132,6 @@ ], 'profile_review' => 'no' ], - - // profilereview test user whose profile is flagged for method_add review 'method_add:g' => [ 'eduPersonPrincipalName' => ['METHOD_ADD@methodidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -180,8 +164,6 @@ ], 'profile_review' => 'no' ], - - // profilereview test user whose profile is flagged for profile review 'profile_review:h' => [ 'eduPersonPrincipalName' => ['METHOD_REVIEW@methodidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -231,8 +213,6 @@ ], 'profile_review' => 'yes' ], - - // mfa test user who does not require mfa 'no_mfa_needed:a' => [ 'eduPersonPrincipalName' => ['NO_MFA_NEEDED@mfaidp'], 'eduPersonTargetID' => ['11111111-1111-1111-1111-111111111111'], @@ -255,8 +235,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa to be set up 'must_set_up_mfa:a' => [ 'eduPersonPrincipalName' => ['MUST_SET_UP_MFA@mfaidp'], 'eduPersonTargetID' => ['22222222-2222-2222-2222-222222222222'], @@ -279,8 +257,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has backup codes 'has_backupcode:a' => [ 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], @@ -311,8 +287,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has backup codes and a manager email 'has_backupcode_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_BACKUPCODE@mfaidp'], 'eduPersonTargetID' => ['33333333-3333-3333-3333-333333333333'], @@ -344,8 +318,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has totp 'has_totp:a' => [ 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -374,8 +346,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has totp and a manager email 'has_totp_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_TOTP@mfaidp'], 'eduPersonTargetID' => ['44444444-4444-4444-4444-444444444444'], @@ -405,8 +375,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has a webauthn 'has_webauthn:a' => [ 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -440,8 +408,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has webauthn and a manager email 'has_webauthn_and_mgr:a' => [ 'eduPersonPrincipalName' => ['HAS_WEBAUTHN@mfaidp'], 'eduPersonTargetID' => ['55555555-5555-5555-5555-555555555555'], @@ -471,8 +437,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has all forms of mfa 'has_all:a' => [ 'eduPersonPrincipalName' => ['has_all@mfaidp'], 'eduPersonTargetID' => ['77777777-7777-7777-7777-777777777777'], @@ -514,8 +478,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who has a rate-limited mfa 'has_rate_limited_mfa:a' => [ 'eduPersonPrincipalName' => ['HAS_RATE_LIMITED_MFA@mfaidp'], 'eduPersonTargetID' => ['88888888-8888-8888-8888-888888888888'], @@ -546,8 +508,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has 4 backup codes 'has_4_backupcodes:a' => [ 'eduPersonPrincipalName' => ['HAS_4_BACKUPCODES@mfaidp'], 'eduPersonTargetID' => ['99999999-9999-9999-9999-999999999999'], @@ -578,8 +538,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has 1 backup code remaining 'has_1_backupcode_only:a' => [ 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_ONLY@mfaidp'], 'eduPersonTargetID' => ['00000010-0010-0010-0010-000000000010'], @@ -610,8 +568,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has one backup code plus another option 'has_1_backupcode_plus:a' => [ 'eduPersonPrincipalName' => ['HAS_1_BACKUPCODE_PLUS@mfaidp'], 'eduPersonTargetID' => ['00000011-0011-0011-0011-000000000011'], @@ -647,8 +603,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has webauthn and totp 'has_webauthn_totp:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], @@ -682,8 +636,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has webauthn, totp and a manager email 'has_webauthn_totp_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp@mfaidp'], 'eduPersonTargetID' => ['00000012-0012-0012-0012-000000000012'], @@ -718,8 +670,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has webauthn and backup codes 'has_webauthn_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], @@ -755,8 +705,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has backup codes and a manager email 'has_webauthn_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000013-0013-0013-0013-000000000013'], @@ -793,8 +741,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has totp and backup codes 'has_webauthn_totp_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], @@ -835,8 +781,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has backup codes, totp, and a manager email 'has_webauthn_totp_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000014-0014-0014-0014-000000000014'], @@ -878,8 +822,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has manager code, webauthn, and a more-recently used totp 'has_mgr_code_webauthn_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_mgr_code_webauthn_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000114-0014-0014-0014-000000000014'], @@ -921,8 +863,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has webauthn and more recently used totp 'has_webauthn_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_webauthn_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000214-0014-0014-0014-000000000014'], @@ -958,8 +898,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has totp and more recently used webauthn 'has_totp_and_more_recently_used_webauthn:a' => [ 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_webauthn@mfaidp'], 'eduPersonTargetID' => ['00000314-0014-0014-0014-000000000014'], @@ -995,8 +933,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has totp and more recently-used backup code 'has_totp_and_more_recently_used_backup_code:a' => [ 'eduPersonPrincipalName' => ['has_totp_and_more_recently_used_backup_code@mfaidp'], 'eduPersonTargetID' => ['00000414-0014-0014-0014-000000000014'], @@ -1034,8 +970,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has backup code and a more recently used totp 'has_backup_code_and_more_recently_used_totp:a' => [ 'eduPersonPrincipalName' => ['has_backup_code_and_more_recently_used_totp@mfaidp'], 'eduPersonTargetID' => ['00000514-0014-0014-0014-000000000014'], @@ -1073,8 +1007,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has totp and backup codes 'has_totp_backupcodes:a' => [ 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], @@ -1110,8 +1042,6 @@ 'options' => [], ], ], - - // mfa test user who requires mfa and has totp, backup codes, and manager email 'has_totp_backupcodes_and_mgr:a' => [ 'eduPersonPrincipalName' => ['has_totp_backupcodes@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], @@ -1148,8 +1078,6 @@ ], 'manager_email' => ['manager@example.com'], ], - - // mfa test user who requires mfa and has backup codes and manager code 'has_mgr_code:a' => [ 'eduPersonPrincipalName' => ['has_mgr_code@mfaidp'], 'eduPersonTargetID' => ['00000015-0015-0015-0015-000000000015'], From c651a0d511276018d62993924f03cc74ea3c8c55 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:19:01 +0800 Subject: [PATCH 53/92] Revert "add comments to describe the reason for disabled test cases [skip ci]" This reverts commit 119cd493ad950d0d46817659ac5d1d7a58f20106. --- features/mfa.feature | 3 --- 1 file changed, 3 deletions(-) diff --git a/features/mfa.feature b/features/mfa.feature index 1ccdba3a..4a134dea 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -141,7 +141,6 @@ Feature: Prompt for MFA credentials | | TOTP | | supports WebAuthn | TOTP | | | TOTP | , backup codes | supports WebAuthn | TOTP | | | | backup codes | supports WebAuthn | backup code | -# The following cases are disabled due to lack of test support for changing web client user agent # | WebAuthn | | | does not support WebAuthn | WebAuthn | # | WebAuthn | , TOTP | | does not support WebAuthn | TOTP | # | WebAuthn | | , backup codes | does not support WebAuthn | backup code | @@ -164,7 +163,6 @@ Feature: Prompt for MFA credentials | TOTP | WebAuthn | supports WebAuthn | WebAuthn | | TOTP | backup code | supports WebAuthn | backup code | | backup code | TOTP | supports WebAuthn | TOTP | -# The following case is disabled due to lack of test support for changing web client user agent # | TOTP | WebAuthn | does not support WebAuthn | TOTP | Scenario: Defaulting to the manager code despite having a used mfa @@ -182,7 +180,6 @@ Feature: Prompt for MFA credentials Examples: | supports WebAuthn or not | should or not | | supports WebAuthn | should not | -# The following case is disabled due to lack of test support for changing web client user agent # | does not support WebAuthn | should | Scenario Outline: When to show the link to send a manager rescue code From 766028ff4d8b87d0aba921d6b1a82b4cda42baf1 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 15 May 2024 17:19:07 +0800 Subject: [PATCH 54/92] Revert "add comment to explain why a profile review is required" This reverts commit 985626671457ba21c9d0213bad75e3d83580b504. --- features/mfa.feature | 2 -- 1 file changed, 2 deletions(-) diff --git a/features/mfa.feature b/features/mfa.feature index 4a134dea..a61a61ab 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -216,7 +216,6 @@ Feature: Prompt for MFA credentials Given I provide credentials that have a manager code And I login When I submit the correct manager code - # because profile review is required after using a manager code: And I click the remind-me-later button Then I should end up at my intended destination @@ -227,7 +226,6 @@ Feature: Prompt for MFA credentials And I click the Request Assistance link And I click the Send a code link When I submit the correct manager code - # because profile review is required after using a manager code: And I click the remind-me-later button Then I should end up at my intended destination From e0941ca5f8a1c62de01a4971c2f0aa824926aa75 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 17 May 2024 11:44:10 +0800 Subject: [PATCH 55/92] in namespace, change SilInternational to Sil --- composer.json | 2 +- development/idp-local/config/authsources.php | 2 +- development/idp-local/metadata/saml20-idp-hosted.php | 2 +- features/bootstrap/MfaContext.php | 2 +- features/fakes/FakeIdBrokerClient.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index a28cafb2..804b5b82 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "vendor/yiisoft/yii2/Yii.php" ], "psr-4": { - "SilInternational\\SspBase\\Features\\": "features/" + "Sil\\SspBase\\Features\\": "features/" } }, "config": { diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 54cadf20..28c8124a 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -1,6 +1,6 @@ Date: Sat, 18 May 2024 11:42:36 +0800 Subject: [PATCH 56/92] copy content from the simplesamlphp-module-sildisco module repo https://github.com/silinternational/simplesamlphp-module-sildisco --- Dockerfile | 2 +- actions-services.yml | 69 +- development/idp3-local/cert/ssp-hub-idp2.crt | 23 + development/idp3-local/cert/ssp-hub-idp2.pem | 28 + development/idp3-local/config/authsources.php | 13 + development/idp3-local/config/config.php | 804 +++++++++ .../idp3-local/metadata/saml20-idp-hosted.php | 25 + .../idp3-local/metadata/saml20-sp-remote.php | 26 + development/init-dynamodb.sh | 14 + development/sp3-local/cert/ssp-hub-sp3.crt | 23 + development/sp3-local/cert/ssp-hub-sp3.pem | 28 + development/sp3-local/config/authsources.php | 49 + development/sp3-local/config/config.php | 803 +++++++++ .../sp3-local/metadata/saml20-idp-remote.php | 28 + docker-compose.yml | 73 +- features/Sp1Idp1Sp2Idp2Sp3.feature | 34 + features/Sp1Idp2Sp2Sp3Idp1.feature | 23 + features/Sp2Idp2Sp1Idp1Sp3.feature | 26 + features/Sp2Idp2Sp1Idp2Sp3.feature | 24 + features/Sp3Idp1Sp1Idp1Sp2Idp2.feature | 24 + features/WwwMetadataCept.feature | 13 + features/ZSp1Idp1BetaSp1Idp3.feature | 14 + features/bootstrap/FeatureContext.php | 6 +- features/bootstrap/SilDiscoContext.php | 65 + .../lib/Auth/Process/AddIdp2NameId.php | 175 ++ modules/sildisco/lib/Auth/Process/LogUser.php | 233 +++ .../sildisco/lib/Auth/Process/TagGroup.php | 90 + .../sildisco/lib/Auth/Process/TrackIdps.php | 38 + modules/sildisco/lib/Auth/Source/SP.php | 1240 ++++++++++++++ modules/sildisco/lib/IdP/SAML2.php | 1515 +++++++++++++++++ modules/sildisco/lib/IdPDisco.php | 214 +++ modules/sildisco/lib/SSOService.php | 47 + modules/sildisco/tests/AddIdpTest.php | 128 ++ modules/sildisco/tests/TagGroupTest.php | 106 ++ .../tests/fixtures/metadata/idp-bad-code.php | 12 + .../tests/fixtures/metadata/idp-bare.php | 7 + .../tests/fixtures/metadata/idp-good.php | 8 + modules/sildisco/tests/phpunit.xml | 30 + modules/sildisco/www/betatest.php | 12 + modules/sildisco/www/disco.php | 9 + modules/sildisco/www/metadata.php | 224 +++ modules/sildisco/www/sp/discoresp.php | 34 + modules/sildisco/www/sp/saml2-acs.php | 273 +++ modules/sildisco/www/sp/saml2-logout.php | 155 ++ 44 files changed, 6782 insertions(+), 5 deletions(-) create mode 100644 development/idp3-local/cert/ssp-hub-idp2.crt create mode 100644 development/idp3-local/cert/ssp-hub-idp2.pem create mode 100644 development/idp3-local/config/authsources.php create mode 100644 development/idp3-local/config/config.php create mode 100644 development/idp3-local/metadata/saml20-idp-hosted.php create mode 100644 development/idp3-local/metadata/saml20-sp-remote.php create mode 100755 development/init-dynamodb.sh create mode 100644 development/sp3-local/cert/ssp-hub-sp3.crt create mode 100644 development/sp3-local/cert/ssp-hub-sp3.pem create mode 100644 development/sp3-local/config/authsources.php create mode 100644 development/sp3-local/config/config.php create mode 100644 development/sp3-local/metadata/saml20-idp-remote.php create mode 100644 features/Sp1Idp1Sp2Idp2Sp3.feature create mode 100644 features/Sp1Idp2Sp2Sp3Idp1.feature create mode 100644 features/Sp2Idp2Sp1Idp1Sp3.feature create mode 100644 features/Sp2Idp2Sp1Idp2Sp3.feature create mode 100644 features/Sp3Idp1Sp1Idp1Sp2Idp2.feature create mode 100644 features/WwwMetadataCept.feature create mode 100644 features/ZSp1Idp1BetaSp1Idp3.feature create mode 100644 features/bootstrap/SilDiscoContext.php create mode 100644 modules/sildisco/lib/Auth/Process/AddIdp2NameId.php create mode 100644 modules/sildisco/lib/Auth/Process/LogUser.php create mode 100644 modules/sildisco/lib/Auth/Process/TagGroup.php create mode 100644 modules/sildisco/lib/Auth/Process/TrackIdps.php create mode 100644 modules/sildisco/lib/Auth/Source/SP.php create mode 100644 modules/sildisco/lib/IdP/SAML2.php create mode 100644 modules/sildisco/lib/IdPDisco.php create mode 100644 modules/sildisco/lib/SSOService.php create mode 100644 modules/sildisco/tests/AddIdpTest.php create mode 100644 modules/sildisco/tests/TagGroupTest.php create mode 100644 modules/sildisco/tests/fixtures/metadata/idp-bad-code.php create mode 100644 modules/sildisco/tests/fixtures/metadata/idp-bare.php create mode 100644 modules/sildisco/tests/fixtures/metadata/idp-good.php create mode 100644 modules/sildisco/tests/phpunit.xml create mode 100644 modules/sildisco/www/betatest.php create mode 100644 modules/sildisco/www/disco.php create mode 100644 modules/sildisco/www/metadata.php create mode 100644 modules/sildisco/www/sp/discoresp.php create mode 100644 modules/sildisco/www/sp/saml2-acs.php create mode 100644 modules/sildisco/www/sp/saml2-logout.php diff --git a/Dockerfile b/Dockerfile index 1c701fdb..d8949c71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,7 +56,7 @@ COPY dockerbuild/ssp-overrides/id.php $SSP_PATH/www/id.php COPY dockerbuild/ssp-overrides/announcement.php $SSP_PATH/announcement/announcement.php COPY tests /data/tests -RUN cp $SSP_PATH/modules/sildisco/sspoverrides/www_saml2_idp/SSOService.php $SSP_PATH/www/saml2/idp/ +RUN cp $SSP_PATH/modules/sildisco/lib/SSOService.php $SSP_PATH/www/saml2/idp/ RUN chmod a+x /data/run.sh /data/run-tests.sh ADD https://github.com/silinternational/config-shim/releases/latest/download/config-shim.gz config-shim.gz diff --git a/actions-services.yml b/actions-services.yml index 756fd699..524bebf5 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -59,7 +59,6 @@ services: # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh - command: /data/run.sh environment: ADMIN_EMAIL: "john_doe@there.com" ADMIN_PASS: "abc123" @@ -143,7 +142,6 @@ 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" environment: @@ -155,6 +153,28 @@ services: SHOW_SAML_ERRORS: "true" THEME_USE: "material:material" + idp3: + image: silintl/ssp-base:develop + volumes: + # Utilize custom certs + - ./development/idp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/idp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + - ./development/idp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + + # Utilize custom metadata + - ./development/idp3-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php + - ./development/idp3-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + + environment: + ADMIN_EMAIL: "john_doe@there.com" + ADMIN_PASS: "c" + SECRET_SALT: "h57fjem34fh*nsJFGNjweJ" + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + IDP_NAME: "IdP3" + ssp-sp1.local: build: . volumes: @@ -179,6 +199,51 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" + sp2: + image: silintl/ssp-base:develop + volumes: + # Utilize custom certs + - ./development/sp2-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp2-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + - ./development/sp2-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp2-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp2 + - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz2 + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + + sp3: + image: silintl/ssp-base:develop + volumes: + # Utilize custom certs + - ./development/sp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + - ./development/sp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp3-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp3 + - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz3 + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + + pwmanager.local: image: silintl/ssp-base:develop volumes: diff --git a/development/idp3-local/cert/ssp-hub-idp2.crt b/development/idp3-local/cert/ssp-hub-idp2.crt new file mode 100644 index 00000000..bcbf054d --- /dev/null +++ b/development/idp3-local/cert/ssp-hub-idp2.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANT +SUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkB +FhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4 +MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldh +eGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2 +ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iL +X/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB ++9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvn +H1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0 +bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSB +OsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1Aw +TjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxl +UQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK9 +9RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuE +ho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR +1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmo +C737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo +240Rh+pDLwxdxPqRAZdeLaUkCQ== +-----END CERTIFICATE----- diff --git a/development/idp3-local/cert/ssp-hub-idp2.pem b/development/idp3-local/cert/ssp-hub-idp2.pem new file mode 100644 index 00000000..7674ef99 --- /dev/null +++ b/development/idp3-local/cert/ssp-hub-idp2.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHmZk3CMSdqQmG +j7l624XLH7aD2uz3qItf8sMLbhoECx03FM24hzWMnunEHEat6kQ0PguSJ60s0GU6 +vWrnSerb511OsJ+agkH70GoE7Fyb2pZ6pZunz9qtSNrE/MrcN9mvEnEhUWPOoUXu +Aj3XL3K8Rb9+otEyG+cfWez43Wd7n3OvXLqH2aYd8F98avVhcB/IQC5XWJyHgB9N ++OqF3AXDGEUssJWfEDRsNs3D/R4YJKIDXc3trTXfFO6st4m7QBBpliywyq9z/XtS +mEfiBYMwRkDDLQR9FIE6wM5gLF43FVqOdZfXzfhUOkEN+mdmP33oaHy3NFDFCBP3 ++mUcqEaLAgMBAAECggEACinHBGdc44483u4oipns6RfXSkV2dXHOjvckeUuE5ZnP +RgO4KeIwltVsn8C01JwuFt7l5e5BQhvmW6RTci1wWPwh4yTZK5vgUjsdetyyJnlt +2hbeox/RSauBADDC/42Udvagbgrf4yCRF/pjPba7x9xhUMhnkH6dORpyF4XmhAPW +TVCA7VVRL5aoEfemiZYOpjPkY135QqI6/PaLbRDUkqUtKdAB2+/XRTF2K8gbb44x +f/wZeqpOG1y82P3aYVM1f3RLQUAS0rKyQJBRB8fHy5LY2z9LAlC8KSp1BAIKtqMT +lUr6MIs2oImrLL0JyvEbcmtZI4MdGgnmkxrjc/8ZYQKBgQD8t18HVfmTu+5HZCuv +NItpLOu/uxm6UwwAwbljtM2K2562wCsu9/tt72V0Ismysz19VUva/FtSqksuOWcA +HC+APHtWMMtsBcQMZGrFHUlJCKv963gu7CoeJvY3mSWm4t8xuZBSz5pAeoeENioH +NrL4+K2+cmVGRNjKIDipN5Ng8QKBgQDKMYrqNmH8/IaIDTi27d1A+1YTEZe/toaP +YbTyyQ731mLwnukAx1MhFgoXe294nXiD3tC0g6ISpFgyUTL5OplIs/yiXks0y5/G +mKxGsVc5qtBQB8utA3i8EzT6x2fIYmuJY2Pj3r6jFFzqOjlILN8ct1v05qjKH+gM +n5C/IC/fOwKBgQCEcubPRXQkxZ5AtHNgxD08xlpYhosZaGUmEGJFq4D+gdRRG66G +U1nnaEzX7VOg4OgdRBMZlqGWVcJJW7RsDlmm8AwERFaZKvxxMj/zR0IdkPnzfvHi +RcxdOTZaNV3SdZ1cxlCp1jyWBqH33Rtx5G0wp8UHx5Tkmziz1udbaNFJQQKBgQDA +EvpE7i59tqJSQkUbObFSVrB44uCGJW2EbawIa0lF1KoerMbpj3B/4MDrd734FZdz +pkobAWUIUojaG9rReYI914Vp9St6VulMLqKRcUxMIuFK9WzdyYt7Fr/gb2c+q4g+ +dmVhBauRnfl6JJ9f2giE7gZ0Cl5TzKWSwE4v0fLIGwKBgGuiI+2j8YOsV4LYyin+ +9p5qmk4gVUe5ohPUKCdPeaZiiQbAJ3l5B3LR2sgV1mOm996Nm9Y0HEback4ISAjz +Nd3TkcwDVaa7GV9pMknM2rK0U6gupbtPAaTMCanXu2VZbKGfQDlkpE3iYvMsuGIW +1ppvkZ+ZtqGlvPGk+CWjr6vu +-----END PRIVATE KEY----- diff --git a/development/idp3-local/config/authsources.php b/development/idp3-local/config/authsources.php new file mode 100644 index 00000000..a5d7d017 --- /dev/null +++ b/development/idp3-local/config/authsources.php @@ -0,0 +1,13 @@ + [ + // The default is to use core:AdminPassword, but it can be replaced with + // any authentication source. + + 'core:AdminPassword', + ], + +]; diff --git a/development/idp3-local/config/config.php b/development/idp3-local/config/config.php new file mode 100644 index 00000000..656009fa --- /dev/null +++ b/development/idp3-local/config/config.php @@ -0,0 +1,804 @@ + $BASE_URL_PATH, + 'certdir' => 'cert/', + 'loggingdir' => 'log/', + 'datadir' => 'data/', + + /* + * A directory where simpleSAMLphp can save temporary files. + * + * SimpleSAMLphp will attempt to create this directory if it doesn't exist. + */ + 'tempdir' => '/tmp/simplesaml', + + + /* + * If you enable this option, simpleSAMLphp will log all sent and received messages + * to the log file. + * + * This option also enables logging of the messages that are encrypted and decrypted. + * + * Note: The messages are logged with the DEBUG log level, so you also need to set + * the 'logging.level' option to LOG_DEBUG. + */ + 'debug' => false, + + /* + * When showerrors is enabled, all error messages and stack traces will be output + * to the browser. + * + * When errorreporting is enabled, a form will be presented for the user to report + * the error to technicalcontact_email. + */ + 'showerrors' => $SHOW_SAML_ERRORS, + 'errorreporting' => false, + + /* + * Custom error show function called from \SimpleSAML\Error\Error::show. + * See docs/simplesamlphp-errorhandling.txt for function code example. + * + * Example: + * 'errors.show_function' => array('\SimpleSAML\Module\example\Error\Show', 'show'), + */ + + /* + * This option allows you to enable validation of XML data against its + * schemas. A warning will be written to the log if validation fails. + */ + 'debug.validatexml' => false, + + /* + * This password must be kept secret, and modified from the default value 123. + * This password will give access to the installation page of simpleSAMLphp with + * metadata listing and diagnostics pages. + * You can also put a hash here; run "bin/pwgen.php" to generate one. + */ + 'auth.adminpassword' => $ADMIN_PASS, + 'admin.protectindexpage' => $ADMIN_PROTECT_INDEX_PAGE, + 'admin.protectmetadata' => true, + + /* + * This is a secret salt used by simpleSAMLphp when it needs to generate a secure hash + * of a value. It must be changed from its default value to a secret value. The value of + * 'secretsalt' can be any valid string of any length. + * + * A possible way to generate a random salt is by running the following command from a unix shell: + * tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo + */ + 'secretsalt' => $SECRET_SALT, + + /* + * Some information about the technical persons running this installation. + * The email address will be used as the recipient address for error reports, and + * also as the technical contact in generated metadata. + */ + 'technicalcontact_name' => $ADMIN_NAME, + 'technicalcontact_email' => $ADMIN_EMAIL, + + /* + * The timezone of the server. This option should be set to the timezone you want + * simpleSAMLphp to report the time in. The default is to guess the timezone based + * on your system timezone. + * + * See this page for a list of valid timezones: http://php.net/manual/en/timezones.php + */ + 'timezone' => $TIMEZONE, + + /* + * Logging. + * + * define the minimum log level to log + * \SimpleSAML\Logger::ERR No statistics, only errors + * \SimpleSAML\Logger::WARNING No statistics, only warnings/errors + * \SimpleSAML\Logger::NOTICE Statistics and errors + * \SimpleSAML\Logger::INFO Verbose logs + * \SimpleSAML\Logger::DEBUG Full debug logs - not reccomended for production + * + * Choose logging handler. + * + * Options: [syslog,file,errorlog] + * + */ + 'logging.level' => \SimpleSAML\Logger::NOTICE, + 'logging.handler' => $LOGGING_HANDLER, + + /* + * Specify the format of the logs. Its use varies depending on the log handler used (for instance, you cannot + * control here how dates are displayed when using the syslog or errorlog handlers), but in general the options + * are: + * + * - %date{}: the date and time, with its format specified inside the brackets. See the PHP documentation + * of the strftime() function for more information on the format. If the brackets are omitted, the standard + * format is applied. This can be useful if you just want to control the placement of the date, but don't care + * about the format. + * + * - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname' + * option below. + * + * - %level: the log level (name or number depending on the handler used). + * + * - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind + * the trailing space). + * + * - %trackid: the track ID, an identifier that allows you to track a single session. + * + * - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the + * $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header. + * + * - %msg: the message to be logged. + * + */ + //'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg', + + /* + * Choose which facility should be used when logging with syslog. + * + * These can be used for filtering the syslog output from simpleSAMLphp into its + * own file by configuring the syslog daemon. + * + * See the documentation for openlog (http://php.net/manual/en/function.openlog.php) for available + * facilities. Note that only LOG_USER is valid on windows. + * + * The default is to use LOG_LOCAL5 if available, and fall back to LOG_USER if not. + */ + 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, + + /* + * The process name that should be used when logging to syslog. + * The value is also written out by the other logging handlers. + */ + 'logging.processname' => 'simplesamlphp', + + /* Logging: file - Logfilename in the loggingdir from above. + */ + 'logging.logfile' => 'simplesamlphp.log', + + /* (New) statistics output configuration. + * + * This is an array of outputs. Each output has at least a 'class' option, which + * selects the output. + */ + 'statistics.out' => [// Log statistics to the normal log. + /* + [ + 'class' => 'core:Log', + 'level' => 'notice', + ], + */ + // Log statistics to files in a directory. One file per day. + /* + [ + 'class' => 'core:File', + 'directory' => '/var/log/stats', + ], + */ + ], + + + /* + * Enable + * + * Which functionality in simpleSAMLphp do you want to enable. Normally you would enable only + * one of the functionalities below, but in some cases you could run multiple functionalities. + * In example when you are setting up a federation bridge. + */ + 'enable.saml20-idp' => $SAML20_IDP_ENABLE, + 'enable.shib13-idp' => false, + 'enable.adfs-idp' => false, + 'enable.wsfed-sp' => false, + 'enable.authmemcookie' => false, + + + /* + * Module enable configuration + * + * Configuration to override module enabling/disabling. + * + * Example: + * + * 'module.enable' => array( + * // Setting to TRUE enables. + * 'exampleauth' => true, + * // Setting to FALSE disables. + * 'saml' => false, + * // Unset or NULL uses default. + * 'core' => NULL, + * ), + * + */ + + 'module.enable' => [ + // Setting to TRUE enables. + 'authgoogle' => $GOOGLE_ENABLE, + ], + + /* + * This value is the duration of the session in seconds. Make sure that the time duration of + * cookies both at the SP and the IdP exceeds this duration. + */ + 'session.duration' => $SESSION_DURATION, + + /* + * Sets the duration, in seconds, data should be stored in the datastore. As the datastore is used for + * login and logout requests, thid option will control the maximum time these operations can take. + * The default is 4 hours (4*60*60) seconds, which should be more than enough for these operations. + */ + 'session.datastore.timeout' => $SESSION_DATASTORE_TIMEOUT, + + /* + * Sets the duration, in seconds, auth state should be stored. + */ + 'session.state.timeout' => $SESSION_STATE_TIMEOUT, + + /* + * Option to override the default settings for the session cookie name + */ + 'session.cookie.name' => 'SSPSESSID', + + /* + * Expiration time for the session cookie, in seconds. + * + * Defaults to 0, which means that the cookie expires when the browser is closed. + * + * Example: + * 'session.cookie.lifetime' => 30*60, + */ + 'session.cookie.lifetime' => $SESSION_COOKIE_LIFETIME, + + /* + * Limit the path of the cookies. + * + * Can be used to limit the path of the cookies to a specific subdirectory. + * + * Example: + * 'session.cookie.path' => '/simplesaml/', + */ + 'session.cookie.path' => '/', + + /* + * Cookie domain. + * + * Can be used to make the session cookie available to several domains. + * + * Example: + * 'session.cookie.domain' => '.example.org', + */ + 'session.cookie.domain' => null, + + /* + * Set the secure flag in the cookie. + * + * Set this to TRUE if the user only accesses your service + * through https. If the user can access the service through + * both http and https, this must be set to FALSE. + */ + 'session.cookie.secure' => $SECURE_COOKIE, + + /* + * When set to FALSE fallback to transient session on session initialization + * failure, throw exception otherwise. + */ + 'session.disable_fallback' => false, + + /* + * Enable secure POST from HTTPS to HTTP. + * + * If you have some SP's on HTTP and IdP is normally on HTTPS, this option + * enables secure POSTing to HTTP endpoint without warning from browser. + * + * For this to work, module.php/core/postredirect.php must be accessible + * also via HTTP on IdP, e.g. if your IdP is on + * https://idp.example.org/ssp/, then + * http://idp.example.org/ssp/module.php/core/postredirect.php must be accessible. + */ + 'enable.http_post' => false, + + /* + * Options to override the default settings for php sessions. + */ + 'session.phpsession.cookiename' => null, + 'session.phpsession.savepath' => null, + 'session.phpsession.httponly' => true, + + /* + * Option to override the default settings for the auth token cookie + */ + 'session.authtoken.cookiename' => 'SSPAUTHTOKEN', + + /* + * Options for remember me feature for IdP sessions. Remember me feature + * has to be also implemented in authentication source used. + * + * Option 'session.cookie.lifetime' should be set to zero (0), i.e. cookie + * expires on browser session if remember me is not checked. + * + * Session duration ('session.duration' option) should be set according to + * 'session.rememberme.lifetime' option. + * + * It's advised to use remember me feature with session checking function + * defined with 'session.check_function' option. + */ + 'session.rememberme.enable' => false, + 'session.rememberme.checked' => false, + 'session.rememberme.lifetime' => $SESSION_REMEMBERME_LIFETIME, + + /** + * Custom function for session checking called on session init and loading. + * See docs/simplesamlphp-advancedfeatures.txt for function code example. + * + * Example: + * 'session.check_function' => array('\SimpleSAML\Module\example\Util', 'checkSession'), + */ + + /* + * Languages available, RTL languages, and what language is default + */ + 'language.available' => array( + 'en', 'es', 'fr', 'pt', + ), + 'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'), + 'language.default' => 'en', + + /* + * Options to override the default settings for the language parameter + */ + 'language.parameter.name' => 'language', + 'language.parameter.setcookie' => true, + + /* + * Options to override the default settings for the language cookie + */ + 'language.cookie.name' => 'language', + 'language.cookie.domain' => null, + 'language.cookie.path' => '/', + 'language.cookie.lifetime' => (60 * 60 * 24 * 900), + + /** + * Custom getLanguage function called from \SimpleSAML\XHTML\Template::getLanguage(). + * Function should return language code of one of the available languages or NULL. + * See \SimpleSAML\XHTML\Template::getLanguage() source code for more info. + * + * This option can be used to implement a custom function for determining + * the default language for the user. + * + * Example: + * 'language.get_language_function' => array('\SimpleSAML\Module\example\Template', 'getLanguage'), + */ + + /* + * Extra dictionary for attribute names. + * This can be used to define local attributes. + * + * The format of the parameter is a string with :. + * + * Specifying this option will cause us to look for modules//dictionaries/.definition.json + * The dictionary should look something like: + * + * { + * "firstattribute": { + * "en": "English name", + * "no": "Norwegian name" + * }, + * "secondattribute": { + * "en": "English name", + * "no": "Norwegian name" + * } + * } + * + * Note that all attribute names in the dictionary must in lowercase. + * + * Example: 'attributes.extradictionary' => 'ourmodule:ourattributes', + */ + 'attributes.extradictionary' => null, + + /* + * Which theme directory should be used? + */ + 'theme.use' => $THEME_USE, + + + /* + * Default IdP for WS-Fed. + */ + // 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost', + + /* + * Whether the discovery service should allow the user to save his choice of IdP. + */ + 'idpdisco.enableremember' => true, + 'idpdisco.rememberchecked' => true, + + // Disco service only accepts entities it knows. + 'idpdisco.validate' => true, + + 'idpdisco.extDiscoveryStorage' => null, + + /* + * IdP Discovery service look configuration. + * Wether to display a list of idp or to display a dropdown box. For many IdP' a dropdown box + * gives the best use experience. + * + * When using dropdown box a cookie is used to highlight the previously chosen IdP in the dropdown. + * This makes it easier for the user to choose the IdP + * + * Options: [links,dropdown] + * + */ + 'idpdisco.layout' => 'links', + + /* + * Whether simpleSAMLphp should sign the response or the assertion in SAML 1.1 authentication + * responses. + * + * The default is to sign the assertion element, but that can be overridden by setting this + * option to TRUE. It can also be overridden on a pr. SP basis by adding an option with the + * same name to the metadata of the SP. + */ + 'shib13.signresponse' => true, + + + /* + * Authentication processing filters that will be executed for all IdPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.idp' => [ + /* Enable the authproc filter below to add URN Prefixces to all attributes + 10 => array( + 'class' => 'core:AttributeMap', 'addurnprefix' + ), */ + /* Enable the authproc filter below to automatically generated eduPersonTargetedID. + 20 => 'core:TargetedID', + */ + + // Adopts language from attribute to use in UI + 30 => 'core:LanguageAdaptor', + + /* Add a realm attribute from edupersonprincipalname + 40 => 'core:AttributeRealm', + */ + 45 => [ + 'class' => 'core:StatisticsWithAttribute', + 'attributename' => 'realm', + 'type' => 'saml20-idp-SSO', + ], + + // Add one to help with testing + 50 => [ + 'class' => 'core:AttributeAdd', + 'eduPersonPrincipalName' => 'TEST_ADMIN', + 'urn:oid:0.9.2342.19200300.100.1.3' => 'test_admin@idp3.org', + 'uid' => '333366', + ], + + // Use the uid value to populate the nameid entry + 60 => [ + 'class' => 'saml:AttributeNameID', + 'attribute' => 'uid', + 'Format' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + ], + + /* + * Search attribute "distinguishedName" for pattern and replaces if found + + 70 => array( + 'class' => 'core:AttributeAlter', + 'pattern' => '/OU=studerende/', + 'replacement' => 'Student', + 'subject' => 'distinguishedName', + '%replace', + ), + */ + + /* + * Consent module is enabled (with no permanent storage, using cookies). + + 90 => array( + 'class' => 'consent:Consent', + 'store' => 'consent:Cookie', + 'focus' => 'yes', + 'checked' => true + ), + */ + + + // If language is set in Consent module it will be added as an attribute. + 99 => 'core:LanguageAdaptor', + ], + /* + * Authentication processing filters that will be executed for all SPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.sp' => [ + /* + 10 => array( + 'class' => 'core:AttributeMap', 'removeurnprefix' + ), + */ + + /* + * Generate the 'group' attribute populated from other variables, including eduPersonAffiliation. + 60 => array( + 'class' => 'core:GenerateGroups', 'eduPersonAffiliation' + ), + */ + /* + * All users will be members of 'users' and 'members' + 61 => array( + 'class' => 'core:AttributeAdd', 'groups' => array('users', 'members') + ), + */ + + // Adopts language from attribute to use in UI + 90 => 'core:LanguageAdaptor', + + ], + + + /* + * This option configures the metadata sources. The metadata sources is given as an array with + * different metadata sources. When searching for metadata, simpleSAMPphp will search through + * the array from start to end. + * + * Each element in the array is an associative array which configures the metadata source. + * The type of the metadata source is given by the 'type' element. For each type we have + * different configuration options. + * + * Flat file metadata handler: + * - 'type': This is always 'flatfile'. + * - 'directory': The directory we will load the metadata files from. The default value for + * this option is the value of the 'metadatadir' configuration option, or + * 'metadata/' if that option is unset. + * + * XML metadata handler: + * This metadata handler parses an XML file with either an EntityDescriptor element or an + * EntitiesDescriptor element. The XML file may be stored locally, or (for debugging) on a remote + * web server. + * The XML hetadata handler defines the following options: + * - 'type': This is always 'xml'. + * - 'file': Path to the XML file with the metadata. + * - 'url': The URL to fetch metadata from. THIS IS ONLY FOR DEBUGGING - THERE IS NO CACHING OF THE RESPONSE. + * + * + * Examples: + * + * This example defines two flatfile sources. One is the default metadata directory, the other + * is a metadata directory with autogenerated metadata files. + * + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'flatfile', 'directory' => 'metadata-generated'), + * ), + * + * This example defines a flatfile source and an XML source. + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'xml', 'file' => 'idp.example.org-idpMeta.xml'), + * ), + * + * + * Default: + * 'metadata.sources' => array( + * array('type' => 'flatfile') + * ), + */ + 'metadata.sources' => [ + ['type' => 'flatfile'], + ], + + + /* + * Configure the datastore for simpleSAMLphp. + * + * - 'phpsession': Limited datastore, which uses the PHP session. + * - 'memcache': Key-value datastore, based on memcache. + * - 'sql': SQL datastore, using PDO. + * + * The default datastore is 'phpsession'. + * + * (This option replaces the old 'session.handler'-option.) + */ + 'store.type' => 'phpsession', + + + /* + * The DSN the sql datastore should connect to. + * + * See http://www.php.net/manual/en/pdo.drivers.php for the various + * syntaxes. + */ + 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3', + + /* + * The username and password to use when connecting to the database. + */ + 'store.sql.username' => null, + 'store.sql.password' => null, + + /* + * The prefix we should use on our tables. + */ + 'store.sql.prefix' => 'simpleSAMLphp', + + + /* + * Configuration for the MemcacheStore class. This allows you to store + * multiple redudant copies of sessions on different memcache servers. + * + * 'memcache_store.servers' is an array of server groups. Every data + * item will be mirrored in every server group. + * + * Each server group is an array of servers. The data items will be + * load-balanced between all servers in each server group. + * + * Each server is an array of parameters for the server. The following + * options are available: + * - 'hostname': This is the hostname or ip address where the + * memcache server runs. This is the only required option. + * - 'port': This is the port number of the memcache server. If this + * option isn't set, then we will use the 'memcache.default_port' + * ini setting. This is 11211 by default. + * - 'weight': This sets the weight of this server in this server + * group. http://php.net/manual/en/function.Memcache-addServer.php + * contains more information about the weight option. + * - 'timeout': The timeout for this server. By default, the timeout + * is 3 seconds. + * + * Example of redudant configuration with load balancing: + * This configuration makes it possible to lose both servers in the + * a-group or both servers in the b-group without losing any sessions. + * Note that sessions will be lost if one server is lost from both the + * a-group and the b-group. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'mc_a1'), + * array('hostname' => 'mc_a2'), + * ), + * array( + * array('hostname' => 'mc_b1'), + * array('hostname' => 'mc_b2'), + * ), + * ), + * + * Example of simple configuration with only one memcache server, + * running on the same computer as the web server: + * Note that all sessions will be lost if the memcache server crashes. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'localhost'), + * ), + * ), + * + */ + 'memcache_store.servers' => [ + [ + ['hostname' => 'localhost'], + ], + ], + + + /* + * This value is the duration data should be stored in memcache. Data + * will be dropped from the memcache servers when this time expires. + * The time will be reset every time the data is written to the + * memcache servers. + * + * This value should always be larger than the 'session.duration' + * option. Not doing this may result in the session being deleted from + * the memcache servers while it is still in use. + * + * Set this value to 0 if you don't want data to expire. + * + * Note: The oldest data will always be deleted if the memcache server + * runs out of storage space. + */ + 'memcache_store.expires' => 36 * (60 * 60), // 36 hours. + + + /* + * Should signing of generated metadata be enabled by default. + * + * Metadata signing can also be enabled for a individual SP or IdP by setting the + * same option in the metadata for the SP or IdP. + */ + 'metadata.sign.enable' => true, + + /* + * The default key & certificate which should be used to sign generated metadata. These + * are files stored in the cert dir. + * These values can be overridden by the options with the same names in the SP or + * IdP metadata. + * + * If these aren't specified here or in the metadata for the SP or IdP, then + * the 'certificate' and 'privatekey' option in the metadata will be used. + * if those aren't set, signing of metadata will fail. + */ + 'metadata.sign.privatekey' => 'ssp-hub.pem', + 'metadata.sign.privatekey_pass' => null, + 'metadata.sign.certificate' => 'ssp-hub.crt', + + + /* + * Proxy to use for retrieving URLs. + * + * Example: + * 'proxy' => 'tcp://proxy.example.com:5100' + */ + 'proxy' => null, + + /* + * Array of domains that are allowed when generating links or redirections + * to URLs. simpleSAMLphp will use this option to determine whether to + * to consider a given URL valid or not, but you should always validate + * URLs obtained from the input on your own (i.e. ReturnTo or RelayState + * parameters obtained from the $_REQUEST array). + * + * Set to NULL to disable checking of URLs. + * + * simpleSAMLphp will automatically add your own domain (either by checking + * it dinamically, or by using the domain defined in the 'baseurlpath' + * directive, the latter having precedence) to the list of trusted domains, + * in case this option is NOT set to NULL. In that case, you are explicitly + * telling simpleSAMLphp to verify URLs. + * + * Set to an empty array to disallow ALL redirections or links pointing to + * an external URL other than your own domain. + * + * Example: + * 'trusted.url.domains' => array('sp.example.com', 'app.example.com'), + */ + 'trusted.url.domains' => null, + +]; diff --git a/development/idp3-local/metadata/saml20-idp-hosted.php b/development/idp3-local/metadata/saml20-idp-hosted.php new file mode 100644 index 00000000..37d487af --- /dev/null +++ b/development/idp3-local/metadata/saml20-idp-hosted.php @@ -0,0 +1,25 @@ + '__DEFAULT__', + + // X.509 key and certificate. Relative to the cert directory. + 'privatekey' => 'ssp-hub-idp2.pem', + 'certificate' => 'ssp-hub-idp2.crt', + + /* + * Authentication source to use. Must be one that is configured in + * 'config/authsources.php'. + */ + 'auth' => 'admin', +]; diff --git a/development/idp3-local/metadata/saml20-sp-remote.php b/development/idp3-local/metadata/saml20-sp-remote.php new file mode 100644 index 00000000..f8c12aae --- /dev/null +++ b/development/idp3-local/metadata/saml20-sp-remote.php @@ -0,0 +1,26 @@ + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'AssertionConsumerService' => 'http://ssp-hub.local/module.php/sildisco/sp/saml2-acs.php/hub-discovery', + 'SingleLogoutService' => 'http://ssp-hub.local/module.php/sildisco/sp/saml2-logout.php/hub-discovery', + 'certData' => 'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', +]; + +/* + * IdP Hub for automated tests + */ +$metadata['hub4tests'] = array( + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'AssertionConsumerService' => 'http://hub4tests/module.php/sildisco/sp/saml2-acs.php/hub-discovery', + 'SingleLogoutService' => 'http://hub4tests/module.php/sildisco/sp/saml2-logout.php/hub-discovery', + 'certData' => 'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', +); \ No newline at end of file diff --git a/development/init-dynamodb.sh b/development/init-dynamodb.sh new file mode 100755 index 00000000..f944c583 --- /dev/null +++ b/development/init-dynamodb.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env ash + +# Create data table +aws dynamodb create-table --table-name sildisco_local_user-log \ + --attribute-definitions AttributeName=ID,AttributeType=S \ + --key-schema AttributeName=ID,KeyType=HASH \ + --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10 \ + --endpoint-url http://dynamo:8000 + + +# Enable Time to Live +aws dynamodb update-time-to-live --table-name sildisco_local_user-log \ + --time-to-live-specification "Enabled=true,AttributeName=ExpiresAt" \ + --endpoint-url http://dynamo:8000 \ No newline at end of file diff --git a/development/sp3-local/cert/ssp-hub-sp3.crt b/development/sp3-local/cert/ssp-hub-sp3.crt new file mode 100644 index 00000000..21ed1c2b --- /dev/null +++ b/development/sp3-local/cert/ssp-hub-sp3.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANT +SUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkB +FhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3 +MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldh +eGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2 +ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYos +gyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/c +esppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoK +GjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBg +dk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I +47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1Aw +TjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYc +MbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+X +rgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVw +PUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298Hu +vsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejt +FBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkh +T0DHCOw67GP97MWzceyFw+n9Vg== +-----END CERTIFICATE----- diff --git a/development/sp3-local/cert/ssp-hub-sp3.pem b/development/sp3-local/cert/ssp-hub-sp3.pem new file mode 100644 index 00000000..14cce0d7 --- /dev/null +++ b/development/sp3-local/cert/ssp-hub-sp3.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDS76ZdZLy9SQqO +0lwrWF3SIZba9tO9iiyDIN2oKVxLPCltYmDUDUKG6JAh/tL7ltYvYA3jMyh6b8lO +ftwT2hy2pTfhkkDI/9x6ymkD0bncX2Afhd2g+Cdaxvy91aigMNST5qxztqH9fAqL +FA1wQs8NmQ5trn6mqgoaMzF8vslDr4/osH7Ei3O8sFdTj6idsY8gXqmZnxLvia3Q +nrZjqd7jQEfHwV2u0GB2TlMVwN51bT2tEmtMWmLeEZJSRyOqPzgH6gsBvTxnRIeg +QsynliHtODlBnZz5Lwjju1CNXvas2FRiEjIW4ibNKDbtHtpEnomPqwrjMSDQCLhW +E7hGaDLPAgMBAAECggEBALkdC5kmkORkt1lDjxOTBzMjuyoKNyQ9oHarXxr2wUJd +V9Xg4iz2Pg37BpJu+WVFqE4HM+jRupJIjBfRCP57CXvYXsQc/7HlqO4xuBtb8IpP +QSIo7qkXXiIyQxet68A5WjU52NnryxmTxATt4iVE3ESIr7rdydQloZwAlUtue15j +mWOyznPK+l4lOeRyibYtFGoHzp4cdhUU2oWxFOEht7F4aGz2SMVGkFfrnRAShmyO +DYliGhEAVFWfnQwXsggXp7c0uBuvrb/kYZc1z6THQ4COBbzbp6NPOJu4yRez+IIs +lNbSYaV9N5EduNt5yayOI1s1Fi8wRm30f+Mgcb/zT2ECgYEA9/P0kl/Tho6ixTnx +furvdvzEW218UderluBooAzEmPKyy1rQQ/rPOcnGYg6SccYSFnJpp5LC128BpQWe +3v4j5c2XsK99PtP5aem5s2NsiZH/ehuTAJnXz1korG5hVGv+bpqK5KhaB7/a3Tm0 +JxVo/fx6iHsHXDGcxAiCzgy36+kCgYEA2cgmrXzqyK9SyPTuFgDjm750h9B7poyJ +DICmoo6FzXPHfjxJyP0xOXtq+nT0ujEDoq/kxs8iLCCW+FSfFuzITylg//0zESvE +9mcfMc6UFO8rS088CtyrsqtIOWG5xYOjrcUA92tiqkWMnklssFxjeNIrojsf/sIJ +AdzYXmx6zfcCgYEA6YP2jLf0xV+lyesFFgt6VOw+nQBiuc1My34y6rC7onPHkR7I +z4zxBrKRxB2HK+FnfX5pJKliGHRx7xF5Cvf7pNxYBM1xPe9ykJ3PBzQWrwUxvrUj +X8iDZ8LHPIWD4ncGmvGu5yPqDixQmlJS6RAP3kuettRvHROYWULOtfFicakCgYAJ +hhk66QWTdSdXpm5rA+rwOqn57oIZzHeJ1m5zGWx8iZ2lxZkscvYeH2mUPl0db1tL +WAnXL+O8rkgr3/d9FynDXHnjd/0tuQ5KAER69x++sp7gEjz79J6Fl7v21nE7VABq +bv0V1Nphu9zkZy2boM6wz/Acjh1eFLo0HKZRqsjMDQKBgQDDiB5QL94aVx/NTJ+y +FWTUmzwJ4HvnOxh2bmFomtU76FhevbK/R6aq85gHPQuD541KpIobTGBJlk9BPelX +V6BicmA/DGxd8aCg99Bn8haTUBTUNAWQJ5FdFmICHY5xul3oHHAs33FCNnx1ETOY +uA/4kr7SqT4LqoMXbsth3UHQhA== +-----END PRIVATE KEY----- diff --git a/development/sp3-local/config/authsources.php b/development/sp3-local/config/authsources.php new file mode 100644 index 00000000..aaf9e770 --- /dev/null +++ b/development/sp3-local/config/authsources.php @@ -0,0 +1,49 @@ + array( + // The default is to use core:AdminPassword, but it can be replaced with + // any authentication source. + + 'core:AdminPassword', + ), + + + // An authentication source which can authenticate against both SAML 2.0 + // and Shibboleth 1.3 IdPs. + 'ssp-hub' => array( + 'saml:SP', + + // The entity ID of this SP. + // Can be NULL/unset, in which case an entity ID is generated based on the metadata URL. + 'entityID' => 'http://ssp-sp3.local', + + // The entity ID of the IdP this should SP should contact. + // Can be NULL/unset, in which case the user will be shown a list of available IdPs. + 'idp' => 'ssp-hub.local', + + // The URL to the discovery service. + // Can be NULL/unset, in which case a builtin discovery service will be used. + 'discoURL' => null, + ), + + 'hub4tests' => array( + 'saml:SP', + + // The entity ID of this SP. + // Can be NULL/unset, in which case an entity ID is generated based on the metadata URL. + 'entityID' => 'http://ssp-sp3.local', + + // The entity ID of the IdP this should SP should contact. + // Can be NULL/unset, in which case the user will be shown a list of available IdPs. + 'idp' => 'hub4tests', + + // The URL to the discovery service. + // Can be NULL/unset, in which case a builtin discovery service will be used. + 'discoURL' => null, + ), + + +); diff --git a/development/sp3-local/config/config.php b/development/sp3-local/config/config.php new file mode 100644 index 00000000..c773de78 --- /dev/null +++ b/development/sp3-local/config/config.php @@ -0,0 +1,803 @@ + $BASE_URL_PATH, + 'certdir' => 'cert/', + 'loggingdir' => 'log/', + 'datadir' => 'data/', + + /* + * A directory where simpleSAMLphp can save temporary files. + * + * SimpleSAMLphp will attempt to create this directory if it doesn't exist. + */ + 'tempdir' => '/tmp/simplesaml', + + + /* + * If you enable this option, simpleSAMLphp will log all sent and received messages + * to the log file. + * + * This option also enables logging of the messages that are encrypted and decrypted. + * + * Note: The messages are logged with the DEBUG log level, so you also need to set + * the 'logging.level' option to LOG_DEBUG. + */ + 'debug' => false, + + /* + * When showerrors is enabled, all error messages and stack traces will be output + * to the browser. + * + * When errorreporting is enabled, a form will be presented for the user to report + * the error to technicalcontact_email. + */ + 'showerrors' => $SHOW_SAML_ERRORS, + 'errorreporting' => false, + + /* + * Custom error show function called from \SimpleSAML\Error\Error::show. + * See docs/simplesamlphp-errorhandling.txt for function code example. + * + * Example: + * 'errors.show_function' => array('\SimpleSAML\Module\example\Error\Show', 'show'), + */ + + /* + * This option allows you to enable validation of XML data against its + * schemas. A warning will be written to the log if validation fails. + */ + 'debug.validatexml' => false, + + /* + * This password must be kept secret, and modified from the default value 123. + * This password will give access to the installation page of simpleSAMLphp with + * metadata listing and diagnostics pages. + * You can also put a hash here; run "bin/pwgen.php" to generate one. + */ + 'auth.adminpassword' => $ADMIN_PASS, + 'admin.protectindexpage' => $ADMIN_PROTECT_INDEX_PAGE, + 'admin.protectmetadata' => true, + + /* + * This is a secret salt used by simpleSAMLphp when it needs to generate a secure hash + * of a value. It must be changed from its default value to a secret value. The value of + * 'secretsalt' can be any valid string of any length. + * + * A possible way to generate a random salt is by running the following command from a unix shell: + * tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo + */ + 'secretsalt' => $SECRET_SALT, + + /* + * Some information about the technical persons running this installation. + * The email address will be used as the recipient address for error reports, and + * also as the technical contact in generated metadata. + */ + 'technicalcontact_name' => $ADMIN_NAME, + 'technicalcontact_email' => $ADMIN_EMAIL, + + /* + * The timezone of the server. This option should be set to the timezone you want + * simpleSAMLphp to report the time in. The default is to guess the timezone based + * on your system timezone. + * + * See this page for a list of valid timezones: http://php.net/manual/en/timezones.php + */ + 'timezone' => $TIMEZONE, + + /* + * Logging. + * + * define the minimum log level to log + * \SimpleSAML\Logger::ERR No statistics, only errors + * \SimpleSAML\Logger::WARNING No statistics, only warnings/errors + * \SimpleSAML\Logger::NOTICE Statistics and errors + * \SimpleSAML\Logger::INFO Verbose logs + * \SimpleSAML\Logger::DEBUG Full debug logs - not reccomended for production + * + * Choose logging handler. + * + * Options: [syslog,file,errorlog] + * + */ + 'logging.level' => \SimpleSAML\Logger::NOTICE, + 'logging.handler' => $LOGGING_HANDLER, + + /* + * Specify the format of the logs. Its use varies depending on the log handler used (for instance, you cannot + * control here how dates are displayed when using the syslog or errorlog handlers), but in general the options + * are: + * + * - %date{}: the date and time, with its format specified inside the brackets. See the PHP documentation + * of the strftime() function for more information on the format. If the brackets are omitted, the standard + * format is applied. This can be useful if you just want to control the placement of the date, but don't care + * about the format. + * + * - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname' + * option below. + * + * - %level: the log level (name or number depending on the handler used). + * + * - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind + * the trailing space). + * + * - %trackid: the track ID, an identifier that allows you to track a single session. + * + * - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the + * $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header. + * + * - %msg: the message to be logged. + * + */ + //'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg', + + /* + * Choose which facility should be used when logging with syslog. + * + * These can be used for filtering the syslog output from simpleSAMLphp into its + * own file by configuring the syslog daemon. + * + * See the documentation for openlog (http://php.net/manual/en/function.openlog.php) for available + * facilities. Note that only LOG_USER is valid on windows. + * + * The default is to use LOG_LOCAL5 if available, and fall back to LOG_USER if not. + */ + 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, + + /* + * The process name that should be used when logging to syslog. + * The value is also written out by the other logging handlers. + */ + 'logging.processname' => 'simplesamlphp', + + /* Logging: file - Logfilename in the loggingdir from above. + */ + 'logging.logfile' => 'simplesamlphp.log', + + /* (New) statistics output configuration. + * + * This is an array of outputs. Each output has at least a 'class' option, which + * selects the output. + */ + 'statistics.out' => [// Log statistics to the normal log. + /* + [ + 'class' => 'core:Log', + 'level' => 'notice', + ], + */ + // Log statistics to files in a directory. One file per day. + /* + [ + 'class' => 'core:File', + 'directory' => '/var/log/stats', + ], + */ + ], + + + /* + * Enable + * + * Which functionality in simpleSAMLphp do you want to enable. Normally you would enable only + * one of the functionalities below, but in some cases you could run multiple functionalities. + * In example when you are setting up a federation bridge. + */ + 'enable.saml20-idp' => $SAML20_IDP_ENABLE, + 'enable.shib13-idp' => false, + 'enable.adfs-idp' => false, + 'enable.wsfed-sp' => false, + 'enable.authmemcookie' => false, + + + /* + * Module enable configuration + * + * Configuration to override module enabling/disabling. + * + * Example: + * + * 'module.enable' => array( + * // Setting to TRUE enables. + * 'exampleauth' => true, + * // Setting to FALSE disables. + * 'saml' => false, + * // Unset or NULL uses default. + * 'core' => NULL, + * ), + * + */ + + 'module.enable' => [ + // Setting to TRUE enables. + 'authgoogle' => $GOOGLE_ENABLE, + ], + + /* + * This value is the duration of the session in seconds. Make sure that the time duration of + * cookies both at the SP and the IdP exceeds this duration. + */ + 'session.duration' => $SESSION_DURATION, + + /* + * Sets the duration, in seconds, data should be stored in the datastore. As the datastore is used for + * login and logout requests, thid option will control the maximum time these operations can take. + * The default is 4 hours (4*60*60) seconds, which should be more than enough for these operations. + */ + 'session.datastore.timeout' => $SESSION_DATASTORE_TIMEOUT, + + /* + * Sets the duration, in seconds, auth state should be stored. + */ + 'session.state.timeout' => $SESSION_STATE_TIMEOUT, + + /* + * Option to override the default settings for the session cookie name + */ + 'session.cookie.name' => 'SSPSESSID', + + /* + * Expiration time for the session cookie, in seconds. + * + * Defaults to 0, which means that the cookie expires when the browser is closed. + * + * Example: + * 'session.cookie.lifetime' => 30*60, + */ + 'session.cookie.lifetime' => $SESSION_COOKIE_LIFETIME, + + /* + * Limit the path of the cookies. + * + * Can be used to limit the path of the cookies to a specific subdirectory. + * + * Example: + * 'session.cookie.path' => '/simplesaml/', + */ + 'session.cookie.path' => '/', + + /* + * Cookie domain. + * + * Can be used to make the session cookie available to several domains. + * + * Example: + * 'session.cookie.domain' => '.example.org', + */ + 'session.cookie.domain' => null, + + /* + * Set the secure flag in the cookie. + * + * Set this to TRUE if the user only accesses your service + * through https. If the user can access the service through + * both http and https, this must be set to FALSE. + */ + 'session.cookie.secure' => $SECURE_COOKIE, + + /* + * When set to FALSE fallback to transient session on session initialization + * failure, throw exception otherwise. + */ + 'session.disable_fallback' => false, + + /* + * Enable secure POST from HTTPS to HTTP. + * + * If you have some SP's on HTTP and IdP is normally on HTTPS, this option + * enables secure POSTing to HTTP endpoint without warning from browser. + * + * For this to work, module.php/core/postredirect.php must be accessible + * also via HTTP on IdP, e.g. if your IdP is on + * https://idp.example.org/ssp/, then + * http://idp.example.org/ssp/module.php/core/postredirect.php must be accessible. + */ + 'enable.http_post' => false, + + /* + * Options to override the default settings for php sessions. + */ + 'session.phpsession.cookiename' => null, + 'session.phpsession.savepath' => null, + 'session.phpsession.httponly' => true, + + /* + * Option to override the default settings for the auth token cookie + */ + 'session.authtoken.cookiename' => 'SSPAUTHTOKEN', + + /* + * Options for remember me feature for IdP sessions. Remember me feature + * has to be also implemented in authentication source used. + * + * Option 'session.cookie.lifetime' should be set to zero (0), i.e. cookie + * expires on browser session if remember me is not checked. + * + * Session duration ('session.duration' option) should be set according to + * 'session.rememberme.lifetime' option. + * + * It's advised to use remember me feature with session checking function + * defined with 'session.check_function' option. + */ + 'session.rememberme.enable' => false, + 'session.rememberme.checked' => false, + 'session.rememberme.lifetime' => $SESSION_REMEMBERME_LIFETIME, + + /** + * Custom function for session checking called on session init and loading. + * See docs/simplesamlphp-advancedfeatures.txt for function code example. + * + * Example: + * 'session.check_function' => array('\SimpleSAML\Module\example\Util', 'checkSession'), + */ + + /* + * Languages available, RTL languages, and what language is default + */ + 'language.available' => array( + 'en', 'es', 'fr', 'pt', + ), + 'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'), + 'language.default' => 'en', + + /* + * Options to override the default settings for the language parameter + */ + 'language.parameter.name' => 'language', + 'language.parameter.setcookie' => true, + + /* + * Options to override the default settings for the language cookie + */ + 'language.cookie.name' => 'language', + 'language.cookie.domain' => null, + 'language.cookie.path' => '/', + 'language.cookie.lifetime' => (60 * 60 * 24 * 900), + + /** + * Custom getLanguage function called from \SimpleSAML\XHTML\Template::getLanguage(). + * Function should return language code of one of the available languages or NULL. + * See \SimpleSAML\XHTML\Template::getLanguage() source code for more info. + * + * This option can be used to implement a custom function for determining + * the default language for the user. + * + * Example: + * 'language.get_language_function' => array('\SimpleSAML\Module\example\Template', 'getLanguage'), + */ + + /* + * Extra dictionary for attribute names. + * This can be used to define local attributes. + * + * The format of the parameter is a string with :. + * + * Specifying this option will cause us to look for modules//dictionaries/.definition.json + * The dictionary should look something like: + * + * { + * "firstattribute": { + * "en": "English name", + * "no": "Norwegian name" + * }, + * "secondattribute": { + * "en": "English name", + * "no": "Norwegian name" + * } + * } + * + * Note that all attribute names in the dictionary must in lowercase. + * + * Example: 'attributes.extradictionary' => 'ourmodule:ourattributes', + */ + 'attributes.extradictionary' => null, + + /* + * Which theme directory should be used? + */ + 'theme.use' => $THEME_USE, + + + /* + * Default IdP for WS-Fed. + */ + // 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost', + + /* + * Whether the discovery service should allow the user to save his choice of IdP. + */ + 'idpdisco.enableremember' => true, + 'idpdisco.rememberchecked' => true, + + // Disco service only accepts entities it knows. + 'idpdisco.validate' => true, + + 'idpdisco.extDiscoveryStorage' => null, + + /* + * IdP Discovery service look configuration. + * Wether to display a list of idp or to display a dropdown box. For many IdP' a dropdown box + * gives the best use experience. + * + * When using dropdown box a cookie is used to highlight the previously chosen IdP in the dropdown. + * This makes it easier for the user to choose the IdP + * + * Options: [links,dropdown] + * + */ + 'idpdisco.layout' => 'links', + + /* + * Whether simpleSAMLphp should sign the response or the assertion in SAML 1.1 authentication + * responses. + * + * The default is to sign the assertion element, but that can be overridden by setting this + * option to TRUE. It can also be overridden on a pr. SP basis by adding an option with the + * same name to the metadata of the SP. + */ + 'shib13.signresponse' => true, + + + /* + * Authentication processing filters that will be executed for all IdPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.idp' => [ + /* Enable the authproc filter below to add URN Prefixces to all attributes + 10 => array( + 'class' => 'core:AttributeMap', 'addurnprefix' + ), */ + /* Enable the authproc filter below to automatically generated eduPersonTargetedID. + 20 => 'core:TargetedID', + */ + + // Adopts language from attribute to use in UI + 30 => 'core:LanguageAdaptor', + + /* Add a realm attribute from edupersonprincipalname + 40 => 'core:AttributeRealm', + */ + 45 => [ + 'class' => 'core:StatisticsWithAttribute', + 'attributename' => 'realm', + 'type' => 'saml20-idp-SSO', + ], + + // If no attributes are requested in the SP metadata, then these will be sent through + 50 => [ + 'class' => 'core:AttributeLimit', + 'default' => true, + 'eduPersonPrincipalName', 'sn', 'givenName', 'mail', + ], + + // Use the uid value to populate the nameid entry + 60 => [ + 'class' => 'saml:AttributeNameID', + 'attribute' => 'uid', + 'Format' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + ], + + /* + * Search attribute "distinguishedName" for pattern and replaces if found + + 70 => array( + 'class' => 'core:AttributeAlter', + 'pattern' => '/OU=studerende/', + 'replacement' => 'Student', + 'subject' => 'distinguishedName', + '%replace', + ), + */ + + /* + * Consent module is enabled (with no permanent storage, using cookies). + + 90 => array( + 'class' => 'consent:Consent', + 'store' => 'consent:Cookie', + 'focus' => 'yes', + 'checked' => true + ), + */ + + + // If language is set in Consent module it will be added as an attribute. + 99 => 'core:LanguageAdaptor', + ], + /* + * Authentication processing filters that will be executed for all SPs + * Both Shibboleth and SAML 2.0 + */ + 'authproc.sp' => [ + /* + 10 => array( + 'class' => 'core:AttributeMap', 'removeurnprefix' + ), + */ + + /* + * Generate the 'group' attribute populated from other variables, including eduPersonAffiliation. + 60 => array( + 'class' => 'core:GenerateGroups', 'eduPersonAffiliation' + ), + */ + /* + * All users will be members of 'users' and 'members' + 61 => array( + 'class' => 'core:AttributeAdd', 'groups' => array('users', 'members') + ), + */ + + // Adopts language from attribute to use in UI + 90 => 'core:LanguageAdaptor', + + ], + + + /* + * This option configures the metadata sources. The metadata sources is given as an array with + * different metadata sources. When searching for metadata, simpleSAMPphp will search through + * the array from start to end. + * + * Each element in the array is an associative array which configures the metadata source. + * The type of the metadata source is given by the 'type' element. For each type we have + * different configuration options. + * + * Flat file metadata handler: + * - 'type': This is always 'flatfile'. + * - 'directory': The directory we will load the metadata files from. The default value for + * this option is the value of the 'metadatadir' configuration option, or + * 'metadata/' if that option is unset. + * + * XML metadata handler: + * This metadata handler parses an XML file with either an EntityDescriptor element or an + * EntitiesDescriptor element. The XML file may be stored locally, or (for debugging) on a remote + * web server. + * The XML hetadata handler defines the following options: + * - 'type': This is always 'xml'. + * - 'file': Path to the XML file with the metadata. + * - 'url': The URL to fetch metadata from. THIS IS ONLY FOR DEBUGGING - THERE IS NO CACHING OF THE RESPONSE. + * + * + * Examples: + * + * This example defines two flatfile sources. One is the default metadata directory, the other + * is a metadata directory with autogenerated metadata files. + * + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'flatfile', 'directory' => 'metadata-generated'), + * ), + * + * This example defines a flatfile source and an XML source. + * 'metadata.sources' => array( + * array('type' => 'flatfile'), + * array('type' => 'xml', 'file' => 'idp.example.org-idpMeta.xml'), + * ), + * + * + * Default: + * 'metadata.sources' => array( + * array('type' => 'flatfile') + * ), + */ + 'metadata.sources' => [ + ['type' => 'flatfile'], + ], + + + /* + * Configure the datastore for simpleSAMLphp. + * + * - 'phpsession': Limited datastore, which uses the PHP session. + * - 'memcache': Key-value datastore, based on memcache. + * - 'sql': SQL datastore, using PDO. + * + * The default datastore is 'phpsession'. + * + * (This option replaces the old 'session.handler'-option.) + */ + 'store.type' => 'phpsession', + + + /* + * The DSN the sql datastore should connect to. + * + * See http://www.php.net/manual/en/pdo.drivers.php for the various + * syntaxes. + */ + 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3', + + /* + * The username and password to use when connecting to the database. + */ + 'store.sql.username' => null, + 'store.sql.password' => null, + + /* + * The prefix we should use on our tables. + */ + 'store.sql.prefix' => 'simpleSAMLphp', + + + /* + * Configuration for the MemcacheStore class. This allows you to store + * multiple redudant copies of sessions on different memcache servers. + * + * 'memcache_store.servers' is an array of server groups. Every data + * item will be mirrored in every server group. + * + * Each server group is an array of servers. The data items will be + * load-balanced between all servers in each server group. + * + * Each server is an array of parameters for the server. The following + * options are available: + * - 'hostname': This is the hostname or ip address where the + * memcache server runs. This is the only required option. + * - 'port': This is the port number of the memcache server. If this + * option isn't set, then we will use the 'memcache.default_port' + * ini setting. This is 11211 by default. + * - 'weight': This sets the weight of this server in this server + * group. http://php.net/manual/en/function.Memcache-addServer.php + * contains more information about the weight option. + * - 'timeout': The timeout for this server. By default, the timeout + * is 3 seconds. + * + * Example of redudant configuration with load balancing: + * This configuration makes it possible to lose both servers in the + * a-group or both servers in the b-group without losing any sessions. + * Note that sessions will be lost if one server is lost from both the + * a-group and the b-group. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'mc_a1'), + * array('hostname' => 'mc_a2'), + * ), + * array( + * array('hostname' => 'mc_b1'), + * array('hostname' => 'mc_b2'), + * ), + * ), + * + * Example of simple configuration with only one memcache server, + * running on the same computer as the web server: + * Note that all sessions will be lost if the memcache server crashes. + * + * 'memcache_store.servers' => array( + * array( + * array('hostname' => 'localhost'), + * ), + * ), + * + */ + 'memcache_store.servers' => [ + [ + ['hostname' => 'localhost'], + ], + ], + + + /* + * This value is the duration data should be stored in memcache. Data + * will be dropped from the memcache servers when this time expires. + * The time will be reset every time the data is written to the + * memcache servers. + * + * This value should always be larger than the 'session.duration' + * option. Not doing this may result in the session being deleted from + * the memcache servers while it is still in use. + * + * Set this value to 0 if you don't want data to expire. + * + * Note: The oldest data will always be deleted if the memcache server + * runs out of storage space. + */ + 'memcache_store.expires' => 36 * (60 * 60), // 36 hours. + + + /* + * Should signing of generated metadata be enabled by default. + * + * Metadata signing can also be enabled for a individual SP or IdP by setting the + * same option in the metadata for the SP or IdP. + */ + 'metadata.sign.enable' => true, + + /* + * The default key & certificate which should be used to sign generated metadata. These + * are files stored in the cert dir. + * These values can be overridden by the options with the same names in the SP or + * IdP metadata. + * + * If these aren't specified here or in the metadata for the SP or IdP, then + * the 'certificate' and 'privatekey' option in the metadata will be used. + * if those aren't set, signing of metadata will fail. + */ + 'metadata.sign.privatekey' => 'ssp-hub-sp3.pem', + 'metadata.sign.privatekey_pass' => null, + 'metadata.sign.certificate' => 'ssp-hub-sp3.crt', + + + /* + * Proxy to use for retrieving URLs. + * + * Example: + * 'proxy' => 'tcp://proxy.example.com:5100' + */ + 'proxy' => null, + + /* + * Array of domains that are allowed when generating links or redirections + * to URLs. simpleSAMLphp will use this option to determine whether to + * to consider a given URL valid or not, but you should always validate + * URLs obtained from the input on your own (i.e. ReturnTo or RelayState + * parameters obtained from the $_REQUEST array). + * + * Set to NULL to disable checking of URLs. + * + * simpleSAMLphp will automatically add your own domain (either by checking + * it dinamically, or by using the domain defined in the 'baseurlpath' + * directive, the latter having precedence) to the list of trusted domains, + * in case this option is NOT set to NULL. In that case, you are explicitly + * telling simpleSAMLphp to verify URLs. + * + * Set to an empty array to disallow ALL redirections or links pointing to + * an external URL other than your own domain. + * + * Example: + * 'trusted.url.domains' => array('sp.example.com', 'app.example.com'), + */ + 'trusted.url.domains' => null, + +]; diff --git a/development/sp3-local/metadata/saml20-idp-remote.php b/development/sp3-local/metadata/saml20-idp-remote.php new file mode 100644 index 00000000..fb1c762f --- /dev/null +++ b/development/sp3-local/metadata/saml20-idp-remote.php @@ -0,0 +1,28 @@ + 'http://ssp-hub.local/saml2/idp/SSOService.php', + 'SingleSignOnService' => 'http://ssp-hub.local/saml2/idp/SSOService.php', + 'SingleLogoutService' => 'http://ssp-hub.local/saml2/idp/SingleLogoutService.php', + 'certData' =>'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', + +]; + +/* + * For automated testing + */ +$metadata['hub4tests'] = [ + 'SingleSignOnService' => 'http://hub4tests/saml2/idp/SSOService.php', + 'SingleLogoutService' => 'http://hub4tests/saml2/idp/SingleLogoutService.php', + 'certData' =>'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', +]; diff --git a/docker-compose.yml b/docker-compose.yml index 39e10f9c..652dba21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -224,7 +224,6 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - command: /data/run.sh ports: - "8086:80" environment: @@ -236,6 +235,31 @@ services: SHOW_SAML_ERRORS: "true" THEME_USE: "material:material" + idp3: + image: silintl/ssp-base:develop + volumes: + # Utilize custom certs + - ./development/idp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/idp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + - ./development/idp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + + # Utilize custom metadata + - ./development/idp3-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php + - ./development/idp3-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + ports: + - "8087:80" + env_file: + - local.env + environment: + ADMIN_EMAIL: "john_doe@there.com" + ADMIN_PASS: "c" + SECRET_SALT: "h57fjem34fh*nsJFGNjweJ" + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + IDP_NAME: "IdP3" + ssp-sp1.local: build: . volumes: @@ -297,6 +321,31 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" + sp3: + image: silintl/ssp-base:develop + volumes: + # Utilize custom certs + - ./development/sp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert + + # Utilize custom configs + - ./development/sp3-local/config/config.php:/data/vendor/simplesamlphp/simplesamlphp/config/config.php + - ./development/sp3-local/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php + + # Utilize custom metadata + - ./development/sp3-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php + ports: + - "8083:80" + env_file: + - local.env + environment: + - ADMIN_EMAIL=john_doe@there.com + - ADMIN_PASS=sp3 + - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz3 + - SECURE_COOKIE=false + - SHOW_SAML_ERRORS=true + - SAML20_IDP_ENABLE=false + - ADMIN_PROTECT_INDEX_PAGE=false + pwmanager.local: image: silintl/ssp-base:develop volumes: @@ -359,6 +408,28 @@ services: MYSQL_USER: "user" MYSQL_PASSWORD: "pass" + dynamo: + image: cnadiminti/dynamodb-local + command: "-sharedDb -inMemory" + hostname: dynamo + ports: + - "8000:8000" + environment: + reschedule: on-node-failure + + init-dynamo: + image: garland/aws-cli-docker + command: "/init-dynamodb.sh" + volumes: + - ./development/init-dynamodb.sh:/init-dynamodb.sh + depends_on: + - dynamo + environment: + - AWS_ACCESS_KEY_ID=0 + - AWS_SECRET_ACCESS_KEY=0 + - AWS_DEFAULT_REGION=us-east-1 + - AWS_DYNAMODB_ENDPOINT=http://dynamo:8000 + networks: default: driver: bridge diff --git a/features/Sp1Idp1Sp2Idp2Sp3.feature b/features/Sp1Idp1Sp2Idp2Sp3.feature new file mode 100644 index 00000000..8d53769f --- /dev/null +++ b/features/Sp1Idp1Sp2Idp2Sp3.feature @@ -0,0 +1,34 @@ +Feature: Ensure I can login to Sp1 through Idp1, must login to Sp2 through Idp2 and am already logged in for Sp3. + + Scenario: Login to SP1 through IDP1 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP1" + And I click on the "IdP 1" tile + And I login using password "a" + Then I should see "test_admin@idp1.org" + + Scenario: After IDP1 login, go to SP2 through IDP2 + Given I have authenticated with IDP1 for SP1 + When I go to the SP2 login page + And I should see "Enter your username and password" + And I login using password "b" + Then I should see "test_admin@idp2.org" + + Scenario: After IDP1 login, go directly to SP3 without credentials + Given I have authenticated with IDP1 for SP1 + When I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP3" + And I click on the "IdP 1" tile + Then I should see "test_admin@idp1.org" + + Scenario: Logout of IDP1 + Given I have authenticated with IDP1 for SP1 + When I log out of IDP1 + Then I should see "You have been logged out." + + Scenario: Logout of IDP2 + Given I have authenticated with IDP2 for SP2 + When I log out of IDP2 + Then I should see "You have been logged out." diff --git a/features/Sp1Idp2Sp2Sp3Idp1.feature b/features/Sp1Idp2Sp2Sp3Idp1.feature new file mode 100644 index 00000000..9f83916f --- /dev/null +++ b/features/Sp1Idp2Sp2Sp3Idp1.feature @@ -0,0 +1,23 @@ +Feature: Ensure I can login to Sp1 through Idp2, am already logged in for Sp2, and must login to Sp3 through Idp1. + + Scenario: Login to SP1 through IDP2 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP1" + And I click on the "IdP 2" tile + And I login using password "b" + Then I should see "test_admin@idp2.org" + + Scenario: After IDP2 login, go directly to SP2 without credentials + Given I have authenticated with IDP2 for SP1 + When I go to the SP2 login page + Then I should see "test_admin@idp2.org" + + Scenario: After IDP2 login, go to SP3 through IDP1 + Given I have authenticated with IDP2 for SP1 + When I go to the SP3 login page + And I should see "to continue to SP3" + And I click on the "IdP 1" tile + And I should see "Enter your username and password" + And I login using password "a" + Then I should see "test_admin@idp1.org" diff --git a/features/Sp2Idp2Sp1Idp1Sp3.feature b/features/Sp2Idp2Sp1Idp1Sp3.feature new file mode 100644 index 00000000..24e6498c --- /dev/null +++ b/features/Sp2Idp2Sp1Idp1Sp3.feature @@ -0,0 +1,26 @@ +Feature: Ensure I can login to Sp2 through Idp2, must login to Sp1 if I choose Idp1, and don't need to login for Sp3. + + Scenario: Login to SP2 through IDP2 + When I go to the SP2 login page + And I should see "Enter your username and password" + And I login using password "b" + Then I should see "test_admin@idp2.org" + + Scenario: Login to SP1 through IDP1 + Given I have authenticated with IDP2 for SP2 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I click on the "IdP 1" tile + And I should see "Enter your username and password" + And I login using password "a" + Then I should see "test_admin@idp1.org" + + Scenario: After IDP2 login, go directly to SP3 without credentials + Given I have authenticated with IDP2 for SP2 + And I have authenticated with IDP1 for SP1 + And I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP3" + And I click on the "IdP 1" tile + Then I should see "test_admin@idp1.org" + diff --git a/features/Sp2Idp2Sp1Idp2Sp3.feature b/features/Sp2Idp2Sp1Idp2Sp3.feature new file mode 100644 index 00000000..c7e785ba --- /dev/null +++ b/features/Sp2Idp2Sp1Idp2Sp3.feature @@ -0,0 +1,24 @@ +Feature: Ensure I can login to Sp2 through Idp2, get discovery page for Sp1, and must login to Sp3 through Idp1. + + Scenario: Login to SP2 through IDP2 + When I go to the SP2 login page + And I should see "Enter your username and password" + And I login using password "b" + Then I should see "test_admin@idp2.org" + + Scenario: Get discovery page for SP1 + Given I have authenticated with IDP2 for SP2 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I click on the "IdP 2" tile + Then I should see "test_admin@idp2.org" + + Scenario: Must login to SP3 through IDP1 + Given I have authenticated with IDP2 for SP2 + When I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I click on the "IdP 1" tile + And I should see "Enter your username and password" + And I login using password "a" + Then I should see "test_admin@idp1.org" + diff --git a/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature b/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature new file mode 100644 index 00000000..b7041e31 --- /dev/null +++ b/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature @@ -0,0 +1,24 @@ +Feature: Ensure I can login to Sp3 through Idp1, get the discovery page for Sp1 and must login to Sp2 through Idp2. + + Scenario: login to SP3 using IDP1 + When I go to the SP3 login page + And the url should match "sildisco/disco.php" + And I should see "to continue to SP3" + And I click on the "IdP 1" tile + And I should see "Enter your username and password" + And I login using password "a" + Then I should see "test_admin@idp1.org" + + Scenario: having authenticated with IDP1 for SP3, go to SP1 via the discovery page + Given I have authenticated with IDP1 for SP3 + When I go to the SP1 login page + And the url should match "sildisco/disco.php" + And I click on the "IdP 1" tile + Then I should see "test_admin@idp1.org" + + Scenario: having authenticated with IDP1 for SP3, login to SP2 using IDP2 + Given I have authenticated with IDP1 for SP3 + When I go to the SP2 login page + And I should see "Enter your username and password" + And I login using password "b" + Then I should see "test_admin@idp2.org" diff --git a/features/WwwMetadataCept.feature b/features/WwwMetadataCept.feature new file mode 100644 index 00000000..0778c0e5 --- /dev/null +++ b/features/WwwMetadataCept.feature @@ -0,0 +1,13 @@ +Feature: Ensure I see the hub's metadata page. + + Scenario: Show the hub's metadata page in default format + When I go to "http://hub4tests/module.php/sildisco/metadata.php" + Then I should see "$metadata['hub4tests']" + + Scenario: Show the hub's metadata page in XML format + When I go to "http://hub4tests/module.php/sildisco/metadata.php?format=xml" + Then I should see the metadata in XML format + + Scenario: Show the hub's metadata page PHP format + When I go to "http://hub4tests/module.php/sildisco/metadata.php?format=php" + Then I should see "$metadata['hub4tests']" diff --git a/features/ZSp1Idp1BetaSp1Idp3.feature b/features/ZSp1Idp1BetaSp1Idp3.feature new file mode 100644 index 00000000..c480ad1f --- /dev/null +++ b/features/ZSp1Idp1BetaSp1Idp3.feature @@ -0,0 +1,14 @@ +Feature: Ensure I don't see IdP 3 at first, but after I go to the Beta Tester page I can see and login through IdP 3. + +Scenario: Normally the IdP3 is disabled + When I go to the "SP1" login page + And the url should match "sildisco/disco.php" + Then the "div" element should contain "IdP 3 coming soon" + +Scenario: After going to the "Beta Test" page, IdP3 is available for use + When I go to "http://hub4tests/module.php/sildisco/betatest.php" + And I go to the "SP1" login page + And I click on the "IdP 3" tile + And I should see "Enter your username and password" + And I login using password "c" + Then I should see "test_admin@idp3.org" diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 25cda925..0dbf537e 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -16,6 +16,7 @@ class FeatureContext extends MinkContext 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'; + protected const SP3_LOGIN_PAGE = 'http://ssp-sp3.local/module.php/core/authenticate.php?as=ssp-hub'; /** @var Session */ protected $session; @@ -90,7 +91,7 @@ public function iLogInAsAHubAdministrator() $this->logInAs('admin', 'abc123'); } - private function logInAs(string $username, string $password) + protected function logInAs(string $username, string $password) { $this->fillField('username', $username); $this->fillField('password', $password); @@ -149,6 +150,9 @@ public function iGoToTheSpLoginPage($sp) case 'SP2': $this->visit(self::SP2_LOGIN_PAGE); break; + case 'SP3': + $this->visit(self::SP3_LOGIN_PAGE); + break; } } diff --git a/features/bootstrap/SilDiscoContext.php b/features/bootstrap/SilDiscoContext.php new file mode 100644 index 00000000..cfc9e4a0 --- /dev/null +++ b/features/bootstrap/SilDiscoContext.php @@ -0,0 +1,65 @@ +logInAs('admin', $password); + } + + /** + * @Given I have authenticated with IDP1 for :sp + */ + public function iHaveAuthenticatedWithIdp1($sp) + { + $this->iGoToTheSpLoginPage($sp); + $this->iClickOnTheTile('IdP 1'); + $this->logInAs('admin', 'a'); + } + + /** + * @Given I have authenticated with IDP2 for :sp + */ + public function iHaveAuthenticatedWithIdp2($sp) + { + $this->iGoToTheSpLoginPage($sp); + if ($sp != "SP2") { // SP2 only has IDP2 in its IDPList + $this->iClickOnTheTile('IdP 2'); + } + $this->logInAs('admin', 'b'); + } + + /** + * @When I log out of IDP1 + */ + public function iLogOutOfIdp1() + { + $this->iGoToTheSpLoginPage('SP3'); + $this->iClickOnTheTile('IdP 1'); + $this->clickLink('Logout'); + $this->assertPageContainsText('You have been logged out.'); + } + + /** + * @When I log out of IDP2 + */ + public function iLogOutOfIdp2() + { + $this->iGoToTheSpLoginPage('SP2'); + $this->clickLink('Logout'); + $this->assertPageContainsText('You have been logged out.'); + } + + /** + * @Then I should see the metadata in XML format + */ + public function iShouldSeeTheMetadataInXmlFormat() + { + $xml = $this->getSession()->getDriver()->getContent(); + assert(str_contains($xml, 'entityID="hub4tests"')); + } + +} diff --git a/modules/sildisco/lib/Auth/Process/AddIdp2NameId.php b/modules/sildisco/lib/Auth/Process/AddIdp2NameId.php new file mode 100644 index 00000000..0f816211 --- /dev/null +++ b/modules/sildisco/lib/Auth/Process/AddIdp2NameId.php @@ -0,0 +1,175 @@ + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + * + */ +class AddIdp2NameId extends \SimpleSAML\Auth\ProcessingFilter { + + const IDP_KEY = "saml:sp:IdP"; // the key that points to the entity id in the state + + // the metadata key for the IDP's Namespace code (i.e. short name) + const IDP_CODE_KEY = 'IDPNamespace'; + + const DELIMITER = '@'; // The symbol between the NameID proper and the Idp code. + + const SP_NAMEID_ATTR = 'saml:sp:NameID'; // The key for the NameID + + const VALUE_KEY = 'Value'; // The value key for the NamedID entry + + const ERROR_PREFIX = "AddIdp2NameId: "; // Text to go at the beginning of error messages + + const FORMAT_KEY = 'Format'; + + /** + * What NameQualifier should be used. + * Can be one of: + * - a string: The qualifier to use. + * - FALSE: Do not include a NameQualifier. This is the default. + * - TRUE: Use the IdP entity ID. + * + * @var string|bool + */ + private $nameQualifier; + + + /** + * What SPNameQualifier should be used. + * Can be one of: + * - a string: The qualifier to use. + * - FALSE: Do not include a SPNameQualifier. + * - TRUE: Use the SP entity ID. This is the default. + * + * @var string|bool + */ + private $spNameQualifier; + + + /** + * The format of this NameID. + * + * This property must be initialized the subclass. + * + * @var string + */ + protected $format; + + + /** + * Initialize this filter, parse configuration. + * + * @param array $config Configuration information about this filter. + * @param mixed $reserved For future use. + */ + public function __construct($config, $reserved) { + parent::__construct($config, $reserved); + assert('is_array($config)'); + + if (isset($config['NameQualifier'])) { + $this->nameQualifier = $config['NameQualifier']; + } else { + $this->nameQualifier = false; + } + + if (isset($config['SPNameQualifier'])) { + $this->spNameQualifier = $config['SPNameQualifier']; + } else { + $this->spNameQualifier = true; + } + + $this->format = Null; + if ( ! empty($config[self::FORMAT_KEY])) { + $this->format = (string) $config[self::FORMAT_KEY]; + } + } + + /** + * @param $nameId \SAML2\XML\saml\NameID + * @param $IDPNamespace string + * + * Modifies the nameID object by adding text to the end of its value attribute + */ + public function appendIdp($nameId, $IDPNamespace) { + + $suffix = self::DELIMITER . $IDPNamespace; + $value = $nameId->getValue(); + $nameId->setValue($value . $suffix); + return; + } + + + /** + * Apply filter to copy attributes. + * + * @param array &$state The current state array + */ + public function process(&$state) { + assert('is_array($state)'); + + $samlIDP = $state[self::IDP_KEY]; + + if (empty($state[self::SP_NAMEID_ATTR])) { + \SimpleSAML\Logger::warning( + self::SP_NAMEID_ATTR . ' attribute not available from ' . + $samlIDP . '.' + ); + return; + } + + // Get the potential IDPs from idp remote metadata + $metadataPath = __DIR__ . '/../../../../../metadata'; + + // If a unit test sends a different metadataPath, use it + if (isset($state['metadataPath'])) { + $metadataPath = $state['metadataPath']; + } + $idpEntries = \Sil\SspUtils\Metadata::getIdpMetadataEntries($metadataPath); + + $idpEntry = $idpEntries[$samlIDP]; + + // The IDP metadata must have an IDPNamespace entry + if ( ! isset($idpEntry[self::IDP_CODE_KEY])) { + throw new \SimpleSAML\Error\Exception(self::ERROR_PREFIX . "Missing required metadata entry: " . + self::IDP_CODE_KEY . "."); + } + + // IDPNamespace must be a non-empty string + if ( ! is_string($idpEntry[self::IDP_CODE_KEY])) { + throw new \SimpleSAML\Error\Exception(self::ERROR_PREFIX . "Required metadata " . + "entry, " . self::IDP_CODE_KEY . ", must be a non-empty string."); + } + + // IDPNamespace must not have special characters in it + if ( ! preg_match("/^[A-Za-z0-9_-]+$/", $idpEntry[self::IDP_CODE_KEY])) { + throw new \SimpleSAML\Error\Exception(self::ERROR_PREFIX . "Required metadata " . + "entry, " . self::IDP_CODE_KEY . ", must not be empty or contain anything except " . + "letters, numbers, hyphens and underscores."); + } + + $IDPNamespace = $idpEntry[self::IDP_CODE_KEY]; + + $nameId = $state[self::SP_NAMEID_ATTR]; + self::appendIdp($nameId, $IDPNamespace); + + $format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'; + + if ( ! empty($this->format)) { + $format = $this->format; + } elseif ( ! empty($nameId->Format)) { + $format = $nameId->Format; + } + + $state['saml:NameID'][$format] = $nameId; + + } + +} diff --git a/modules/sildisco/lib/Auth/Process/LogUser.php b/modules/sildisco/lib/Auth/Process/LogUser.php new file mode 100644 index 00000000..c1cc2a55 --- /dev/null +++ b/modules/sildisco/lib/Auth/Process/LogUser.php @@ -0,0 +1,233 @@ +dynamoEndpoint = $config[self::DYNAMO_ENDPOINT_KEY] ?? null; + $this->dynamoRegion = $config[self::DYNAMO_REGION_KEY] ?? null; + $this->dynamoLogTable = $config[self::DYNAMO_LOG_TABLE_KEY] ?? null; + } + + /** + * Log info for a user's login to Dyanmodb + * + * @param array &$state The current state array + */ + public function process(&$state) { + if (! $this->configsAreValid()) { + return; + } + + $awsKey = getenv(self::AWS_ACCESS_KEY_ID_ENV); + if (! $awsKey ) { + \SimpleSAML\Logger::error(self::AWS_ACCESS_KEY_ID_ENV . " environment variable is required for LogUser."); + return; + } + $awsSecret = getenv(self::AWS_SECRET_ACCESS_KEY_ENV); + if (! $awsSecret ) { + \SimpleSAML\Logger::error(self::AWS_SECRET_ACCESS_KEY_ENV . " environment variable is required for LogUser."); + return; + } + + assert(is_array($state)); + + // Get the SP's entity id + $spEntityId = "SP entity ID not available"; + if (! empty($state['saml:sp:State']['SPMetadata']['entityid'])) { + $spEntityId = $state['saml:sp:State']['SPMetadata']['entityid']; + } + + $sdkConfig = [ + 'region' => $this->dynamoRegion, + 'version' => 'latest', + 'credentials' => [ + 'key' => $awsKey, + 'secret' => $awsSecret, + ], + ]; + + if (!empty($this->dynamoEndpoint)) { + $sdkConfig['endpoint'] = $this->dynamoEndpoint; + } + + $sdk = new \Aws\Sdk($sdkConfig); + + $dynamodb = $sdk->createDynamoDb(); + $marshaler = new Marshaler(); + + $userAttributes = $this->getUserAttributes($state); + + $logContents = array_merge( + $userAttributes, + [ + "ID" => uniqid(), + "IDP" => $this->getIdp($state), + "SP" => $spEntityId, + "Time" => date("Y-m-d H:i:s"), + "ExpiresAt" => time() + self::SECONDS_PER_YEAR, + ] + ); + + $logJson = json_encode($logContents); + + $item = $marshaler->marshalJson($logJson); + + $params = [ + 'TableName' => $this->dynamoLogTable, + 'Item' => $item, + ]; + + try { + $result = $dynamodb->putItem($params); + } catch (\Exception $e) { + \SimpleSAML\Logger::error("Unable to add item: ". $e->getMessage()); + } + } + + private function configsAreValid() { + $msg = ' config value not provided to LogUser.'; + + if (empty($this->dynamoRegion)) { + \SimpleSAML\Logger::error(self::DYNAMO_REGION_KEY . $msg); + return false; + } + + if (empty($this->dynamoLogTable)) { + \SimpleSAML\Logger::error(self::DYNAMO_LOG_TABLE_KEY . $msg); + return false; + } + + return true; + } + + private function getIdp(&$state) { + if (empty($state[self::IDP_KEY])) { + return 'No IDP available'; + } + + $samlIDP = $state[self::IDP_KEY]; + + // Get the potential IDPs from idp remote metadata + $metadataPath = __DIR__ . '/../../../../../metadata'; + + // If a unit test sends a different metadataPath, use it + if (isset($state['metadataPath'])) { + $metadataPath = $state['metadataPath']; + } + $idpEntries = \Sil\SspUtils\Metadata::getIdpMetadataEntries($metadataPath); + + // Get the IDPNamespace or else just use the IDP's entity ID + $idpEntry = $idpEntries[$samlIDP]; + + // If the IDPNamespace entry is a string, use it + if (isset($idpEntry[self::IDP_CODE_KEY]) && is_string($idpEntry[self::IDP_CODE_KEY])) { + return $idpEntry[self::IDP_CODE_KEY]; + } + + // Default, use the idp's entity ID + return $samlIDP; + } + + // Get the current user's common name attribute and/or eduPersonPrincipalName and/or employeeNumber + private function getUserAttributes($state) { + $attributes = $state['Attributes']; + + $cn = $this->getAttributeFrom($attributes, 'urn:oid:2.5.4.3', 'cn'); + + $eduPersonPrincipalName = $this->getAttributeFrom( + $attributes, + 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', + 'eduPersonPrincipalName' + ); + + $employeeNumber = $this->getAttributeFrom( + $attributes, + 'urn:oid:2.16.840.1.113730.3.1.3', + 'employeeNumber' + ); + + $userAttrs = []; + + $userAttrs = $this->addUserAttribute($userAttrs, "CN", $cn); + $userAttrs = $this->addUserAttribute($userAttrs, "EduPersonPrincipalName", $eduPersonPrincipalName); + $userAttrs = $this->addUserAttribute($userAttrs, "EmployeeNumber", $employeeNumber); + + return $userAttrs; + } + + private function getAttributeFrom($attributes, $oidKey, $friendlyKey) { + if (!empty($attributes[$oidKey])) { + return $attributes[$oidKey][0]; + } + + if (!empty($attributes[$friendlyKey])) { + return $attributes[$friendlyKey][0]; + } + + return ''; + } + + // Dynamodb seems to complain when a value is an empty string. + // This ensures that only attributes with a non empty value get included. + private function addUserAttribute($attributes, $attrKey, $attr) { + if (!empty($attr)) { + $attributes[$attrKey] = $attr; + } + + return $attributes; + } + +} diff --git a/modules/sildisco/lib/Auth/Process/TagGroup.php b/modules/sildisco/lib/Auth/Process/TagGroup.php new file mode 100644 index 00000000..59402907 --- /dev/null +++ b/modules/sildisco/lib/Auth/Process/TagGroup.php @@ -0,0 +1,90 @@ +getData($sessionDataType, $sessionKey); + if ( ! $sessionValue) { + $sessionValue = []; + } + + // Will we need to wrap the idp in htmlspecialchars() + $authIdps = $session->getAuthData("hub-discovery", "saml:AuthenticatingAuthority"); + + if ( ! in_array($authIdps[0], $sessionValue)) { + $sessionValue[$authIdps[0]] = $authIdps[0]; + } + + $session->setData($sessionDataType, $sessionKey, $sessionValue); + } + + +} diff --git a/modules/sildisco/lib/Auth/Source/SP.php b/modules/sildisco/lib/Auth/Source/SP.php new file mode 100644 index 00000000..b9781797 --- /dev/null +++ b/modules/sildisco/lib/Auth/Source/SP.php @@ -0,0 +1,1240 @@ +getMetadataURL(); + } + + /* For compatibility with code that assumes that $metadata->getString('entityid') + * gives the entity id. */ + $config['entityid'] = $config['entityID']; + + $this->metadata = Configuration::loadFromArray( + $config, + 'authsources[' . var_export($this->authId, true) . ']' + ); + $this->entityId = $this->metadata->getString('entityID'); + $this->idp = $this->metadata->getString('idp', null); + $this->discoURL = $this->metadata->getString('discoURL', null); + $this->disable_scoping = $this->metadata->getBoolean('disable_scoping', false); + + if (empty($this->discoURL) && Module::isModuleEnabled('discojuice')) { + $this->discoURL = Module::getModuleURL('discojuice/central.php'); + } + } + + + /** + * Retrieve the URL to the metadata of this SP. + * + * @return string The metadata URL. + */ + public function getMetadataURL() + { + return Module::getModuleURL('saml/sp/metadata.php/' . urlencode($this->authId)); + } + + + /** + * Retrieve the entity id of this SP. + * + * @return string The entity id of this SP. + */ + public function getEntityId() + { + return $this->entityId; + } + + + /** + * Retrieve the metadata array of this SP, as a remote IdP would see it. + * + * @return array The metadata array for its use by a remote IdP. + */ + public function getHostedMetadata() + { + $entityid = $this->getEntityId(); + $metadata = [ + 'entityid' => $entityid, + 'metadata-set' => 'saml20-sp-remote', + 'SingleLogoutService' => $this->getSLOEndpoints(), + 'AssertionConsumerService' => $this->getACSEndpoints(), + ]; + + // add NameIDPolicy + if ($this->metadata->hasValue('NameIDPolicy')) { + $format = $this->metadata->getValue('NameIDPolicy'); + if (is_array($format)) { + $metadata['NameIDFormat'] = Configuration::loadFromArray($format)->getString( + 'Format', + Constants::NAMEID_TRANSIENT + ); + } elseif (is_string($format)) { + $metadata['NameIDFormat'] = $format; + } + } + + // add attributes + $name = $this->metadata->getLocalizedString('name', null); + $attributes = $this->metadata->getArray('attributes', []); + if ($name !== null) { + if (!empty($attributes)) { + $metadata['name'] = $name; + $metadata['attributes'] = $attributes; + if ($this->metadata->hasValue('attributes.required')) { + $metadata['attributes.required'] = $this->metadata->getArray('attributes.required'); + } + if ($this->metadata->hasValue('description')) { + $metadata['description'] = $this->metadata->getArray('description'); + } + if ($this->metadata->hasValue('attributes.NameFormat')) { + $metadata['attributes.NameFormat'] = $this->metadata->getString('attributes.NameFormat'); + } + if ($this->metadata->hasValue('attributes.index')) { + $metadata['attributes.index'] = $this->metadata->getInteger('attributes.index'); + } + if ($this->metadata->hasValue('attributes.isDefault')) { + $metadata['attributes.isDefault'] = $this->metadata->getBoolean('attributes.isDefault'); + } + } + } + + // add organization info + $org = $this->metadata->getLocalizedString('OrganizationName', null); + if ($org !== null) { + $metadata['OrganizationName'] = $org; + $metadata['OrganizationDisplayName'] = $this->metadata->getLocalizedString('OrganizationDisplayName', $org); + $metadata['OrganizationURL'] = $this->metadata->getLocalizedString('OrganizationURL', null); + if ($metadata['OrganizationURL'] === null) { + throw new Error\Exception( + 'If OrganizationName is set, OrganizationURL must also be set.' + ); + } + } + + // add contacts + $contacts = $this->metadata->getArray('contacts', []); + foreach ($contacts as $contact) { + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + + // add technical contact + $globalConfig = Configuration::getInstance(); + $email = $globalConfig->getString('technicalcontact_email', 'na@example.org'); + if ($email && $email !== 'na@example.org') { + $contact = [ + 'emailAddress' => $email, + 'name' => $globalConfig->getString('technicalcontact_name', null), + 'contactType' => 'technical', + ]; + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + + // add certificate(s) + $certInfo = Utils\Crypto::loadPublicKey($this->metadata, false, 'new_'); + $hasNewCert = false; + if ($certInfo !== null && array_key_exists('certData', $certInfo)) { + $hasNewCert = true; + $metadata['keys'][] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => true, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => 'new_', + 'url' => Module::getModuleURL( + 'admin/federation/cert', + [ + 'set' => 'saml20-sp-hosted', + 'source' => $this->getAuthId(), + 'prefix' => 'new_' + ] + ), + 'name' => 'sp', + ]; + } + + $certInfo = Utils\Crypto::loadPublicKey($this->metadata); + if ($certInfo !== null && array_key_exists('certData', $certInfo)) { + $metadata['keys'][] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => $hasNewCert ? false : true, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => '', + 'url' => Module::getModuleURL( + 'admin/federation/cert', + [ + 'set' => 'saml20-sp-hosted', + 'source' => $this->getAuthId(), + 'prefix' => '' + ] + ), + 'name' => 'sp', + ]; + } + + // add EntityAttributes extension + if ($this->metadata->hasValue('EntityAttributes')) { + $metadata['EntityAttributes'] = $this->metadata->getArray('EntityAttributes'); + } + + // add UIInfo extension + if ($this->metadata->hasValue('UIInfo')) { + $metadata['UIInfo'] = $this->metadata->getArray('UIInfo'); + } + + // add RegistrationInfo extension + if ($this->metadata->hasValue('RegistrationInfo')) { + $metadata['RegistrationInfo'] = $this->metadata->getArray('RegistrationInfo'); + } + + // add signature options + if ($this->metadata->hasValue('WantAssertionsSigned')) { + $metadata['saml20.sign.assertion'] = $this->metadata->getBoolean('WantAssertionsSigned'); + } + if ($this->metadata->hasValue('redirect.sign')) { + $metadata['redirect.validate'] = $this->metadata->getBoolean('redirect.sign'); + } elseif ($this->metadata->hasValue('sign.authnrequest')) { + $metadata['validate.authnrequest'] = $this->metadata->getBoolean('sign.authnrequest'); + } + + return $metadata; + } + + + /** + * Retrieve the metadata of an IdP. + * + * @param string $entityId The entity id of the IdP. + * @return \SimpleSAML\Configuration The metadata of the IdP. + */ + public function getIdPMetadata($entityId) + { + assert(is_string($entityId)); + + if ($this->idp !== null && $this->idp !== $entityId) { + throw new Error\Exception('Cannot retrieve metadata for IdP ' . + var_export($entityId, true) . ' because it isn\'t a valid IdP for this SP.'); + } + + $metadataHandler = MetaDataStorageHandler::getMetadataHandler(); + + // First, look in saml20-idp-remote. + try { + return $metadataHandler->getMetaDataConfig($entityId, 'saml20-idp-remote'); + } catch (\Exception $e) { + // Metadata wasn't found + Logger::debug('getIdpMetadata: ' . $e->getMessage()); + } + + // Not found in saml20-idp-remote, look in shib13-idp-remote + try { + return $metadataHandler->getMetaDataConfig($entityId, 'shib13-idp-remote'); + } catch (\Exception $e) { + // Metadata wasn't found + Logger::debug('getIdpMetadata: ' . $e->getMessage()); + } + + // Not found + throw new Error\Exception('Could not find the metadata of an IdP with entity ID ' . + var_export($entityId, true)); + } + + + /** + * Retrieve the metadata of this SP. + * + * @return \SimpleSAML\Configuration The metadata of this SP. + */ + public function getMetadata() + { + return $this->metadata; + } + + + /** + * Get a list with the protocols supported by this SP. + * + * @return array + */ + public function getSupportedProtocols() + { + return $this->protocols; + } + + + /** + * Get the AssertionConsumerService endpoints for a given local SP. + * + * @return array + * @throws \Exception + */ + private function getACSEndpoints(): array + { + // If a list of endpoints is specified in config, take that at face value + if ($this->metadata->hasValue('AssertionConsumerService')) { + return $this->metadata->getArray('AssertionConsumerService'); + } + + $endpoints = []; + $default = [ + Constants::BINDING_HTTP_POST, + 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', + Constants::BINDING_HTTP_ARTIFACT, + 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01', + ]; + if ($this->metadata->getString('ProtocolBinding', '') === Constants::BINDING_HOK_SSO) { + $default[] = Constants::BINDING_HOK_SSO; + } + + $bindings = $this->metadata->getArray('acs.Bindings', $default); + $index = 0; + foreach ($bindings as $service) { + switch ($service) { + case Constants::BINDING_HTTP_POST: + $acs = [ + 'Binding' => Constants::BINDING_HTTP_POST, + 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + ]; + if (!in_array(Constants::NS_SAMLP, $this->protocols, true)) { + $this->protocols[] = Constants::NS_SAMLP; + } + break; + case 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post': + $acs = [ + 'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', + 'Location' => Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->getAuthId()), + ]; + if (!in_array('urn:oasis:names:tc:SAML:1.0:profiles:browser-post', $this->protocols, true)) { + $this->protocols[] = 'urn:oasis:names:tc:SAML:1.1:protocol'; + } + break; + case Constants::BINDING_HTTP_ARTIFACT: + $acs = [ + 'Binding' => Constants::BINDING_HTTP_ARTIFACT, + 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + ]; + if (!in_array(Constants::NS_SAMLP, $this->protocols, true)) { + $this->protocols[] = Constants::NS_SAMLP; + } + break; + case 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01': + $acs = [ + 'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01', + 'Location' => Module::getModuleURL( + 'saml/sp/saml1-acs.php/' . $this->getAuthId() . '/artifact' + ), + ]; + if (!in_array('urn:oasis:names:tc:SAML:1.1:protocol', $this->protocols, true)) { + $this->protocols[] = 'urn:oasis:names:tc:SAML:1.1:protocol'; + } + break; + case Constants::BINDING_HOK_SSO: + $acs = [ + 'Binding' => Constants::BINDING_HOK_SSO, + 'Location' => Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->getAuthId()), + 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT, + ]; + if (!in_array(Constants::NS_SAMLP, $this->protocols, true)) { + $this->protocols[] = Constants::NS_SAMLP; + } + break; + default: + $acs = []; + } + $acs['index'] = $index; + $endpoints[] = $acs; + $index++; + } + return $endpoints; + } + + + /** + * Get the SingleLogoutService endpoints available for a given local SP. + * + * @return array + * @throws \SimpleSAML\Error\CriticalConfigurationError + */ + private function getSLOEndpoints(): array + { + $store = Store::getInstance(); + $bindings = $this->metadata->getArray( + 'SingleLogoutServiceBinding', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_SOAP, + ] + ); + $defaultLocation = Module::getModuleURL('saml/sp/saml2-logout.php/' . $this->getAuthId()); + $location = $this->metadata->getString('SingleLogoutServiceLocation', $defaultLocation); + + $endpoints = []; + foreach ($bindings as $binding) { + if ($binding == Constants::BINDING_SOAP && !($store instanceof Store\SQL)) { + // we cannot properly support SOAP logout + continue; + } + $endpoints[] = [ + 'Binding' => $binding, + 'Location' => $location, + ]; + } + return $endpoints; + } + + + /** + * Send a SAML1 SSO request to an IdP. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param array $state The state array for the current authentication. + * @return void + * @deprecated will be removed in a future version + */ + private function startSSO1(Configuration $idpMetadata, array $state): void + { + $idpEntityId = $idpMetadata->getString('entityid'); + + $state['saml:idp'] = $idpEntityId; + + $ar = new Shib13\AuthnRequest(); + $ar->setIssuer($this->entityId); + + $id = Auth\State::saveState($state, 'saml:sp:sso'); + $ar->setRelayState($id); + + $useArtifact = $idpMetadata->getBoolean('saml1.useartifact', null); + if ($useArtifact === null) { + $useArtifact = $this->metadata->getBoolean('saml1.useartifact', false); + } + + if ($useArtifact) { + $shire = Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId . '/artifact'); + } else { + $shire = Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId); + } + + $url = $ar->createRedirect($idpEntityId, $shire); + + Logger::debug('Starting SAML 1 SSO to ' . var_export($idpEntityId, true) . + ' from ' . var_export($this->entityId, true) . '.'); + Utils\HTTP::redirectTrustedURL($url); + } + + + /** + * Send a SAML2 SSO request to an IdP + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param array $state The state array for the current authentication. + * @return void + */ + private function startSSO2(Configuration $idpMetadata, array $state): void + { + if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] < 0) { + Auth\State::throwException( + $state, + new Module\saml\Error\ProxyCountExceeded(Constants::STATUS_RESPONDER) + ); + } + + $ar = Module\saml\Message::buildAuthnRequest($this->metadata, $idpMetadata); + + // GTIS + $ar->setAssertionConsumerServiceURL(Module::getModuleURL('sildisco/sp/saml2-acs.php/'.$this->authId)); + + if (isset($state['\SimpleSAML\Auth\Source.ReturnURL'])) { + $ar->setRelayState($state['\SimpleSAML\Auth\Source.ReturnURL']); + } + + $accr = null; + if ($idpMetadata->getString('AuthnContextClassRef', false)) { + $accr = Utils\Arrays::arrayize($idpMetadata->getString('AuthnContextClassRef')); + } elseif (isset($state['saml:AuthnContextClassRef'])) { + $accr = Utils\Arrays::arrayize($state['saml:AuthnContextClassRef']); + } + + if ($accr !== null) { + $comp = Constants::COMPARISON_EXACT; + if ($idpMetadata->getString('AuthnContextComparison', false)) { + $comp = $idpMetadata->getString('AuthnContextComparison'); + } elseif ( + isset($state['saml:AuthnContextComparison']) + && in_array($state['saml:AuthnContextComparison'], [ + Constants::COMPARISON_EXACT, + Constants::COMPARISON_MINIMUM, + Constants::COMPARISON_MAXIMUM, + Constants::COMPARISON_BETTER, + ], true) + ) { + $comp = $state['saml:AuthnContextComparison']; + } + $ar->setRequestedAuthnContext(['AuthnContextClassRef' => $accr, 'Comparison' => $comp]); + } + + if (isset($state['saml:Audience'])) { + $ar->setAudiences($state['saml:Audience']); + } + if (isset($state['ForceAuthn'])) { + $ar->setForceAuthn((bool) $state['ForceAuthn']); + } + + if (isset($state['isPassive'])) { + $ar->setIsPassive((bool) $state['isPassive']); + } + + if (isset($state['saml:NameID'])) { + if (!is_array($state['saml:NameID']) && !is_a($state['saml:NameID'], NameID::class)) { + throw new Error\Exception('Invalid value of $state[\'saml:NameID\'].'); + } + + $nameId = $state['saml:NameID']; + if (is_array($nameId)) { + // Must be an array > convert to object + + $nid = new NameID(); + if (!array_key_exists('Value', $nameId)) { + throw new \InvalidArgumentException('Missing "Value" in array, cannot create NameID from it.'); + } + + $nid->setValue($nameId['Value']); + if (array_key_exists('NameQualifier', $nameId) && $nameId['NameQualifier'] !== null) { + $nid->setNameQualifier($nameId['NameQualifier']); + } + if (array_key_exists('SPNameQualifier', $nameId) && $nameId['SPNameQualifier'] !== null) { + $nid->setSPNameQualifier($nameId['SPNameQualifier']); + } + if (array_key_exists('SPProvidedID', $nameId) && $nameId['SPProvidedId'] !== null) { + $nid->setSPProvidedID($nameId['SPProvidedID']); + } + if (array_key_exists('Format', $nameId) && $nameId['Format'] !== null) { + $nid->setFormat($nameId['Format']); + } + } else { + $nid = $nameId; + } + + $ar->setNameId($nid); + } + + if (isset($state['saml:NameIDPolicy'])) { + $policy = null; + if (is_string($state['saml:NameIDPolicy'])) { + $policy = [ + 'Format' => (string) $state['saml:NameIDPolicy'], + 'AllowCreate' => true, + ]; + } elseif (is_array($state['saml:NameIDPolicy'])) { + $policy = $state['saml:NameIDPolicy']; + } elseif ($state['saml:NameIDPolicy'] === null) { + $policy = ['Format' => Constants::NAMEID_TRANSIENT]; + } + if ($policy !== null) { + $ar->setNameIdPolicy($policy); + } + } + + $IDPList = []; + $requesterID = []; + + /* Only check for real info for Scoping element if we are going to send Scoping element */ + if ($this->disable_scoping !== true && $idpMetadata->getBoolean('disable_scoping', false) !== true) { + if (isset($state['saml:IDPList'])) { + $IDPList = $state['saml:IDPList']; + } + + if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] !== null) { + $ar->setProxyCount($state['saml:ProxyCount']); + } elseif ($idpMetadata->getInteger('ProxyCount', null) !== null) { + $ar->setProxyCount($idpMetadata->getInteger('ProxyCount', null)); + } elseif ($this->metadata->getInteger('ProxyCount', null) !== null) { + $ar->setProxyCount($this->metadata->getInteger('ProxyCount', null)); + } + + $requesterID = []; + if (isset($state['saml:RequesterID'])) { + $requesterID = $state['saml:RequesterID']; + } + + if (isset($state['core:SP'])) { + $requesterID[] = $state['core:SP']; + } + } else { + Logger::debug('Disabling samlp:Scoping for ' . var_export($idpMetadata->getString('entityid'), true)); + } + + $ar->setIDPList( + array_unique( + array_merge( + $this->metadata->getArray('IDPList', []), + $idpMetadata->getArray('IDPList', []), + (array) $IDPList + ) + ) + ); + + $ar->setRequesterID($requesterID); + + // If the downstream SP has set extensions then use them. + // Otherwise use extensions that might be defined in the local SP (only makes sense in a proxy scenario) + if (isset($state['saml:Extensions']) && count($state['saml:Extensions']) > 0) { + $ar->setExtensions($state['saml:Extensions']); + } else if ($this->metadata->getArray('saml:Extensions', null) !== null) { + $ar->setExtensions($this->metadata->getArray('saml:Extensions')); + } + + $providerName = $this->metadata->getString("ProviderName", null); + if ($providerName !== null) { + $ar->setProviderName($providerName); + } + + + // save IdP entity ID as part of the state + $state['ExpectedIssuer'] = $idpMetadata->getString('entityid'); + + $id = Auth\State::saveState($state, 'saml:sp:sso', true); + $ar->setId($id); + + Logger::debug( + 'Sending SAML 2 AuthnRequest to ' . var_export($idpMetadata->getString('entityid'), true) + ); + + // Select appropriate SSO endpoint + if ($ar->getProtocolBinding() === Constants::BINDING_HOK_SSO) { + /** @var array $dst */ + $dst = $idpMetadata->getDefaultEndpoint( + 'SingleSignOnService', + [ + Constants::BINDING_HOK_SSO + ] + ); + } else { + /** @var array $dst */ + $dst = $idpMetadata->getEndpointPrioritizedByBinding( + 'SingleSignOnService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST, + ] + ); + } + $ar->setDestination($dst['Location']); + + $b = Binding::getBinding($dst['Binding']); + + $this->sendSAML2AuthnRequest($state, $b, $ar); + + assert(false); + } + + + /** + * Function to actually send the authentication request. + * + * This function does not return. + * + * @param array &$state The state array. + * @param \SAML2\Binding $binding The binding. + * @param \SAML2\AuthnRequest $ar The authentication request. + * @return void + */ + public function sendSAML2AuthnRequest(array &$state, Binding $binding, AuthnRequest $ar) + { + $binding->send($ar); + assert(false); + } + + + /** + * Send a SSO request to an IdP. + * + * @param string $idp The entity ID of the IdP. + * @param array $state The state array for the current authentication. + * @return void + */ + public function startSSO($idp, array $state) + { + assert(is_string($idp)); + + $idpMetadata = $this->getIdPMetadata($idp); + + $type = $idpMetadata->getString('metadata-set'); + switch ($type) { + case 'shib13-idp-remote': + $this->startSSO1($idpMetadata, $state); + assert(false); // Should not return + case 'saml20-idp-remote': + $this->startSSO2($idpMetadata, $state); + assert(false); // Should not return + default: + // Should only be one of the known types + assert(false); + } + } + + + /** + * Start an IdP discovery service operation. + * + * @param array $state The state array. + * @return void + */ + private function startDisco(array $state): void + { + $id = Auth\State::saveState($state, 'saml:sp:sso'); + + $discoURL = $this->discoURL; + if ($discoURL === null) { + // Fallback to internal discovery service + $discoURL = Module::getModuleURL('saml/disco.php'); + } + + $returnTo = Module::getModuleURL('sildisco/sp/discoresp.php', ['AuthID' => $id]); // GTIS + + $params = [ + 'entityID' => $this->entityId, + 'return' => $returnTo, + 'returnIDParam' => 'idpentityid' + ]; + + if (isset($state['saml:IDPList'])) { + $params['IDPList'] = $state['saml:IDPList']; + } + + if (isset($state['isPassive']) && $state['isPassive']) { + $params['isPassive'] = 'true'; + } + + Utils\HTTP::redirectTrustedURL($discoURL, $params); + } + + + /** + * Start login. + * + * This function saves the information about the login, and redirects to the IdP. + * + * @param array &$state Information about the current authentication. + * @return void + */ + public function authenticate(&$state) + { + assert(is_array($state)); + + // We are going to need the authId in order to retrieve this authentication source later + $state['saml:sp:AuthId'] = $this->authId; + + $idp = $this->idp; + + if (isset($state['saml:idp'])) { + $idp = (string) $state['saml:idp']; + } + + if (isset($state['saml:IDPList']) && sizeof($state['saml:IDPList']) > 0) { + // we have a SAML IDPList (we are a proxy): filter the list of IdPs available + $mdh = MetaDataStorageHandler::getMetadataHandler(); + $matchedEntities = $mdh->getMetaDataForEntities($state['saml:IDPList'], 'saml20-idp-remote'); + + if (empty($matchedEntities)) { + // all requested IdPs are unknown + throw new Module\saml\Error\NoSupportedIDP( + Constants::STATUS_REQUESTER, + 'None of the IdPs requested are supported by this proxy.' + ); + } + + if (!is_null($idp) && !array_key_exists($idp, $matchedEntities)) { + // the IdP is enforced but not in the IDPList + throw new Module\saml\Error\NoAvailableIDP( + Constants::STATUS_REQUESTER, + 'None of the IdPs requested are available to this proxy.' + ); + } + + if (is_null($idp) && sizeof($matchedEntities) === 1) { + // only one IdP requested or valid + $idp = key($matchedEntities); + } + } + + if ($idp === null) { + $this->startDisco($state); + assert(false); + } + + $this->startSSO($idp, $state); + assert(false); + } + + + /** + * Re-authenticate an user. + * + * This function is called by the IdP to give the authentication source a chance to + * interact with the user even in the case when the user is already authenticated. + * + * @param array &$state Information about the current authentication. + * @return void + */ + public function reauthenticate(array &$state) + { + $session = Session::getSessionFromRequest(); + $data = $session->getAuthState($this->authId); + $data = $session->getAuthState($this->authId); + if ($data === null) { + throw new Error\NoState(); + } + + foreach ($data as $k => $v) { + $state[$k] = $v; + } + + /* + * GTIS + * If this SP is allowed to use more than one IdP, then send to discovery page + */ + $metadataPath = __DIR__ . '/../../../../../metadata'; + + $spEntityId = $state['SPMetadata']['entityid']; + $IDPList = array_keys(DiscoUtils::getIdpsForSp($spEntityId, $metadataPath)); + + if (sizeof($IDPList) > 1) { + $state['LoginCompletedHandler'] = array(SP::class, 'reauthPostLogin'); + $this->authenticate($state); + assert(false); + } + + // GTIS Changed this if block to avoid logging out before authenticating + // with a new IdP + if (sizeof($IDPList) > 0 && + !in_array($state['saml:sp:IdP'], $IDPList, true)) { + /* + * The user has an existing, valid session. However, the list of IdPs + * accessible to this SP does not include the IdP from the existing + * session. + */ + + Logger::warning( + "Reauthentication is needed. The IdP '${state['saml:sp:IdP']}' is not in the IDPList ". + "accessible to this Service Provider '${state['core:SP']}'." + ); + + $state['LoginCompletedHandler'] = array(SP::class, 'reauthPostLogin'); + $this->authenticate($state); + } + // End GTIS + } + + + /** + * Ask the user to log out before being able to log in again with a + * different identity provider. Note that this method is intended for + * instances of SimpleSAMLphp running as a SAML proxy, and therefore + * acting both as an SP and an IdP at the same time. + * + * This method will never return. + * + * @param array $state The state array. + * The following keys must be defined in the array: + * - 'saml:sp:IdPMetadata': a \SimpleSAML\Configuration object containing + * the metadata of the IdP that authenticated the user in the current + * session. + * - 'saml:sp:AuthId': the identifier of the current authentication source. + * - 'core:IdP': the identifier of the local IdP. + * - 'SPMetadata': an array with the metadata of this local SP. + * + * @return void + * @throws \SimpleSAML\Error\NoPassive In case the authentication request was passive. + */ + public static function askForIdPChange(array &$state) + { + assert(array_key_exists('saml:sp:IdPMetadata', $state)); + assert(array_key_exists('saml:sp:AuthId', $state)); + assert(array_key_exists('core:IdP', $state)); + assert(array_key_exists('SPMetadata', $state)); + + if (isset($state['isPassive']) && (bool) $state['isPassive']) { + // passive request, we cannot authenticate the user + throw new Module\saml\Error\NoPassive( + Constants::STATUS_REQUESTER, + 'Reauthentication required' + ); + } + + // save the state WITHOUT a restart URL, so that we don't try an IdP-initiated login if something goes wrong + $id = Auth\State::saveState($state, 'saml:proxy:invalid_idp', true); + $url = Module::getModuleURL('saml/proxy/invalid_session.php'); + Utils\HTTP::redirectTrustedURL($url, ['AuthState' => $id]); + assert(false); + } + + + /** + * Log the user out before logging in again. + * + * This method will never return. + * + * @param array $state The state array. + * @return void + */ + public static function reauthLogout(array $state) + { + Logger::debug('Proxy: logging the user out before re-authentication.'); + + if (isset($state['Responder'])) { + $state['saml:proxy:reauthLogout:PrevResponder'] = $state['Responder']; + } + $state['Responder'] = [SP::class, 'reauthPostLogout']; + + $idp = IdP::getByState($state); + $idp->handleLogoutRequest($state, null); + assert(false); + } + + + /** + * Complete login operation after re-authenticating the user on another IdP. + * + * @param array $state The authentication state. + * @return void + */ + public static function reauthPostLogin(array $state) + { + assert(isset($state['ReturnCallback'])); + + // Update session state + $session = Session::getSessionFromRequest(); + $authId = $state['saml:sp:AuthId']; + $session->doLogin($authId, Auth\State::getPersistentAuthData($state)); + + // resume the login process + call_user_func($state['ReturnCallback'], $state); + assert(false); + } + + + /** + * Post-logout handler for re-authentication. + * + * This method will never return. + * + * @param \SimpleSAML\IdP $idp The IdP we are logging out from. + * @param array &$state The state array with the state during logout. + * @return void + */ + public static function reauthPostLogout(IdP $idp, array $state) + { + assert(isset($state['saml:sp:AuthId'])); + + Logger::debug('Proxy: logout completed.'); + + if (isset($state['saml:proxy:reauthLogout:PrevResponder'])) { + $state['Responder'] = $state['saml:proxy:reauthLogout:PrevResponder']; + } + + /** @var \SimpleSAML\Module\saml\Auth\Source\SP $sp */ + $sp = Auth\Source::getById($state['saml:sp:AuthId'], Module\saml\Auth\Source\SP::class); + + Logger::debug('Proxy: logging in again.'); + $sp->authenticate($state); + assert(false); + } + + + /** + * Start a SAML 2 logout operation. + * + * @param array $state The logout state. + * @return void + */ + public function startSLO2(&$state) + { + assert(is_array($state)); + assert(array_key_exists('saml:logout:IdP', $state)); + assert(array_key_exists('saml:logout:NameID', $state)); + assert(array_key_exists('saml:logout:SessionIndex', $state)); + + $id = Auth\State::saveState($state, 'saml:slosent'); + + $idp = $state['saml:logout:IdP']; + $nameId = $state['saml:logout:NameID']; + $sessionIndex = $state['saml:logout:SessionIndex']; + + $idpMetadata = $this->getIdPMetadata($idp); + + /** @var array $endpoint */ + $endpoint = $idpMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ], + false + ); + if ($endpoint === false) { + Logger::info('No logout endpoint for IdP ' . var_export($idp, true) . '.'); + return; + } + + $lr = Module\saml\Message::buildLogoutRequest($this->metadata, $idpMetadata); + $lr->setNameId($nameId); + $lr->setSessionIndex($sessionIndex); + $lr->setRelayState($id); + $lr->setDestination($endpoint['Location']); + + $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', null); + if ($encryptNameId === null) { + $encryptNameId = $this->metadata->getBoolean('nameid.encryption', false); + } + if ($encryptNameId) { + $lr->encryptNameId(Module\saml\Message::getEncryptionKey($idpMetadata)); + } + + $b = Binding::getBinding($endpoint['Binding']); + $b->send($lr); + + assert(false); + } + + + /** + * Start logout operation. + * + * @param array $state The logout state. + * @return void + */ + public function logout(&$state) + { + assert(is_array($state)); + assert(array_key_exists('saml:logout:Type', $state)); + + $logoutType = $state['saml:logout:Type']; + switch ($logoutType) { + case 'saml1': + // Nothing to do + return; + case 'saml2': + $this->startSLO2($state); + return; + default: + // Should never happen + assert(false); + } + } + + + /** + * Handle a response from a SSO operation. + * + * @param array $state The authentication state. + * @param string $idp The entity id of the IdP. + * @param array $attributes The attributes. + * @return void + */ + public function handleResponse(array $state, $idp, array $attributes) + { + assert(is_string($idp)); + assert(array_key_exists('LogoutState', $state)); + assert(array_key_exists('saml:logout:Type', $state['LogoutState'])); + + $idpMetadata = $this->getIdPMetadata($idp); + + $spMetadataArray = $this->metadata->toArray(); + $idpMetadataArray = $idpMetadata->toArray(); + + /* Save the IdP in the state array. */ + $state['saml:sp:IdP'] = $idp; + $state['PersistentAuthData'][] = 'saml:sp:IdP'; + + $authProcState = [ + 'saml:sp:IdP' => $idp, + 'saml:sp:State' => $state, + 'ReturnCall' => [SP::class, 'onProcessingCompleted'], + + 'Attributes' => $attributes, + 'Destination' => $spMetadataArray, + 'Source' => $idpMetadataArray, + ]; + + if (isset($state['saml:sp:NameID'])) { + $authProcState['saml:sp:NameID'] = $state['saml:sp:NameID']; + } + if (isset($state['saml:sp:SessionIndex'])) { + $authProcState['saml:sp:SessionIndex'] = $state['saml:sp:SessionIndex']; + } + + $pc = new Auth\ProcessingChain($idpMetadataArray, $spMetadataArray, 'sp'); + $pc->processState($authProcState); + + self::onProcessingCompleted($authProcState); + } + + + /** + * Handle a logout request from an IdP. + * + * @param string $idpEntityId The entity ID of the IdP. + * @return void + */ + public function handleLogout($idpEntityId) + { + assert(is_string($idpEntityId)); + + /* Call the logout callback we registered in onProcessingCompleted(). */ + $this->callLogoutCallback($idpEntityId); + } + + + /** + * Handle an unsolicited login operations. + * + * This method creates a session from the information received. It will + * then redirect to the given URL. This is used to handle IdP initiated + * SSO. This method will never return. + * + * @param string $authId The id of the authentication source that received the request. + * @param array $state A state array. + * @param string $redirectTo The URL we should redirect the user to after updating + * the session. The function will check if the URL is allowed, so there is no need to + * manually check the URL on beforehand. Please refer to the 'trusted.url.domains' + * configuration directive for more information about allowing (or disallowing) URLs. + * @return void + */ + public static function handleUnsolicitedAuth($authId, array $state, $redirectTo) + { + assert(is_string($authId)); + assert(is_string($redirectTo)); + + $session = Session::getSessionFromRequest(); + $session->doLogin($authId, Auth\State::getPersistentAuthData($state)); + + Utils\HTTP::redirectUntrustedURL($redirectTo); + } + + + /** + * Called when we have completed the procssing chain. + * + * @param array $authProcState The processing chain state. + * @return void + */ + public static function onProcessingCompleted(array $authProcState) + { + assert(array_key_exists('saml:sp:IdP', $authProcState)); + assert(array_key_exists('saml:sp:State', $authProcState)); + assert(array_key_exists('Attributes', $authProcState)); + + $idp = $authProcState['saml:sp:IdP']; + $state = $authProcState['saml:sp:State']; + + $sourceId = $state['saml:sp:AuthId']; + + /** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */ + $source = Auth\Source::getById($sourceId); + if ($source === null) { + throw new \Exception('Could not find authentication source with id ' . $sourceId); + } + + // Register a callback that we can call if we receive a logout request from the IdP + $source->addLogoutCallback($idp, $state); + + $state['Attributes'] = $authProcState['Attributes']; + + if (isset($state['saml:sp:isUnsolicited']) && (bool) $state['saml:sp:isUnsolicited']) { + if (!empty($state['saml:sp:RelayState'])) { + $redirectTo = $state['saml:sp:RelayState']; + } else { + $redirectTo = $source->getMetadata()->getString('RelayState', '/'); + } + self::handleUnsolicitedAuth($sourceId, $state, $redirectTo); + } + + Auth\Source::completeAuth($state); + } +} diff --git a/modules/sildisco/lib/IdP/SAML2.php b/modules/sildisco/lib/IdP/SAML2.php new file mode 100644 index 00000000..4d975bd6 --- /dev/null +++ b/modules/sildisco/lib/IdP/SAML2.php @@ -0,0 +1,1515 @@ +getConfig(); + + $assertion = self::buildAssertion($idpMetadata, $spMetadata, $state); + + if (isset($state['saml:AuthenticatingAuthority'])) { + $assertion->setAuthenticatingAuthority($state['saml:AuthenticatingAuthority']); + } + + // create the session association (for logout) + $association = [ + 'id' => 'saml:' . $spEntityId, + 'Handler' => '\SimpleSAML\Module\saml\IdP\SAML2', + 'Expires' => $assertion->getSessionNotOnOrAfter(), + 'saml:entityID' => $spEntityId, + 'saml:NameID' => $state['saml:idp:NameID'], + 'saml:SessionIndex' => $assertion->getSessionIndex(), + ]; + + // maybe encrypt the assertion + $assertion = self::encryptAssertion($idpMetadata, $spMetadata, $assertion); + + // create the response + $ar = self::buildResponse($idpMetadata, $spMetadata, $consumerURL); + $ar->setInResponseTo($requestId); + $ar->setRelayState($relayState); + $ar->setAssertions([$assertion]); + + // register the session association with the IdP + $idp->addAssociation($association); + + $statsData = [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'protocol' => 'saml2', + ]; + if (isset($state['saml:AuthnRequestReceivedAt'])) { + $statsData['logintime'] = microtime(true) - $state['saml:AuthnRequestReceivedAt']; + } + Stats::log('saml:idp:Response', $statsData); + + // send the response + $binding = Binding::getBinding($protocolBinding); + $binding->send($ar); + } + + + /** + * Handle authentication error. + * + * \SimpleSAML\Error\Exception $exception The exception. + * + * @param array $state The error state. + * @return void + */ + public static function handleAuthError(Error\Exception $exception, array $state) + { + assert(isset($state['SPMetadata'])); + assert(isset($state['saml:ConsumerURL'])); + assert(array_key_exists('saml:RequestId', $state)); // Can be NULL. + assert(array_key_exists('saml:RelayState', $state)); // Can be NULL. + + $spMetadata = $state["SPMetadata"]; + $spEntityId = $spMetadata['entityid']; + $spMetadata = Configuration::loadFromArray( + $spMetadata, + '$metadata[' . var_export($spEntityId, true) . ']' + ); + + $requestId = $state['saml:RequestId']; + $relayState = $state['saml:RelayState']; + $consumerURL = $state['saml:ConsumerURL']; + $protocolBinding = $state['saml:Binding']; + + $idp = IdP::getByState($state); + + $idpMetadata = $idp->getConfig(); + + $error = \SimpleSAML\Module\saml\Error::fromException($exception); + + Logger::warning("Returning error to SP with entity ID '" . var_export($spEntityId, true) . "'."); + $exception->log(Logger::WARNING); + + $ar = self::buildResponse($idpMetadata, $spMetadata, $consumerURL); + $ar->setInResponseTo($requestId); + $ar->setRelayState($relayState); + + $status = [ + 'Code' => $error->getStatus(), + 'SubCode' => $error->getSubStatus(), + 'Message' => $error->getStatusMessage(), + ]; + $ar->setStatus($status); + + $statsData = [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'protocol' => 'saml2', + 'error' => $status, + ]; + if (isset($state['saml:AuthnRequestReceivedAt'])) { + $statsData['logintime'] = microtime(true) - $state['saml:AuthnRequestReceivedAt']; + } + Stats::log('saml:idp:Response:error', $statsData); + + $binding = Binding::getBinding($protocolBinding); + $binding->send($ar); + } + + + /** + * Find SP AssertionConsumerService based on parameter in AuthnRequest. + * + * @param array $supportedBindings The bindings we allow for the response. + * @param \SimpleSAML\Configuration $spMetadata The metadata for the SP. + * @param string|null $AssertionConsumerServiceURL AssertionConsumerServiceURL from request. + * @param string|null $ProtocolBinding ProtocolBinding from request. + * @param int|null $AssertionConsumerServiceIndex AssertionConsumerServiceIndex from request. + * + * @return array|null Array with the Location and Binding we should use for the response. + */ + private static function getAssertionConsumerService( + array $supportedBindings, + Configuration $spMetadata, + string $AssertionConsumerServiceURL = null, + string $ProtocolBinding = null, + int $AssertionConsumerServiceIndex = null + ): ?array { + /* We want to pick the best matching endpoint in the case where for example + * only the ProtocolBinding is given. We therefore pick endpoints with the + * following priority: + * 1. isDefault="true" + * 2. isDefault unset + * 3. isDefault="false" + */ + $firstNotFalse = null; + $firstFalse = null; + foreach ($spMetadata->getEndpoints('AssertionConsumerService') as $ep) { + if ($AssertionConsumerServiceURL !== null && $ep['Location'] !== $AssertionConsumerServiceURL) { + continue; + } + if ($ProtocolBinding !== null && $ep['Binding'] !== $ProtocolBinding) { + continue; + } + if ($AssertionConsumerServiceIndex !== null && $ep['index'] !== $AssertionConsumerServiceIndex) { + continue; + } + + if (!in_array($ep['Binding'], $supportedBindings, true)) { + /* The endpoint has an unsupported binding. */ + continue; + } + + // we have an endpoint that matches all our requirements. Check if it is the best one + + if (array_key_exists('isDefault', $ep)) { + if ($ep['isDefault'] === true) { + // this is the first matching endpoint with isDefault set to true + return $ep; + } + // isDefault is set to FALSE, but the endpoint is still usable + if ($firstFalse === null) { + // this is the first endpoint that we can use + $firstFalse = $ep; + } + } else { + if ($firstNotFalse === null) { + // this is the first endpoint without isDefault set + $firstNotFalse = $ep; + } + } + } + + if ($firstNotFalse !== null) { + return $firstNotFalse; + } elseif ($firstFalse !== null) { + return $firstFalse; + } + + Logger::warning('Authentication request specifies invalid AssertionConsumerService:'); + if ($AssertionConsumerServiceURL !== null) { + Logger::warning('AssertionConsumerServiceURL: ' . var_export($AssertionConsumerServiceURL, true)); + } + if ($ProtocolBinding !== null) { + Logger::warning('ProtocolBinding: ' . var_export($ProtocolBinding, true)); + } + if ($AssertionConsumerServiceIndex !== null) { + Logger::warning( + 'AssertionConsumerServiceIndex: ' . var_export($AssertionConsumerServiceIndex, true) + ); + } + + // we have no good endpoints. Our last resort is to just use the default endpoint + return $spMetadata->getDefaultEndpoint('AssertionConsumerService', $supportedBindings); + } + + + /** + * Receive an authentication request. + * + * @param \SimpleSAML\IdP $idp The IdP we are receiving it for. + * @return void + * @throws \SimpleSAML\Error\BadRequest In case an error occurs when trying to receive the request. + */ + public static function receiveAuthnRequest(\SimpleSAML\IdP $idp) + { + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + + $supportedBindings = [Constants::BINDING_HTTP_POST]; + if ($idpMetadata->getBoolean('saml20.sendartifact', false)) { + $supportedBindings[] = Constants::BINDING_HTTP_ARTIFACT; + } + if ($idpMetadata->getBoolean('saml20.hok.assertion', false)) { + $supportedBindings[] = Constants::BINDING_HOK_SSO; + } + if ($idpMetadata->getBoolean('saml20.ecp', false)) { + $supportedBindings[] = Constants::BINDING_PAOS; + } + + if (isset($_REQUEST['spentityid']) || isset($_REQUEST['providerId'])) { + /* IdP initiated authentication. */ + + if (isset($_REQUEST['cookieTime'])) { + $cookieTime = (int) $_REQUEST['cookieTime']; + if ($cookieTime + 5 > time()) { + /* + * Less than five seconds has passed since we were + * here the last time. Cookies are probably disabled. + */ + Utils\HTTP::checkSessionCookie(Utils\HTTP::getSelfURL()); + } + } + + $spEntityId = (string) isset($_REQUEST['spentityid']) ? $_REQUEST['spentityid'] : $_REQUEST['providerId']; + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + if (isset($_REQUEST['RelayState'])) { + $relayState = (string) $_REQUEST['RelayState']; + } elseif (isset($_REQUEST['target'])) { + $relayState = (string) $_REQUEST['target']; + } else { + $relayState = null; + } + + if (isset($_REQUEST['binding'])) { + $protocolBinding = (string) $_REQUEST['binding']; + } else { + $protocolBinding = null; + } + + if (isset($_REQUEST['NameIDFormat'])) { + $nameIDFormat = (string) $_REQUEST['NameIDFormat']; + } else { + $nameIDFormat = null; + } + + if (isset($_REQUEST['ConsumerURL'])) { + $consumerURL = (string)$_REQUEST['ConsumerURL']; + } elseif (isset($_REQUEST['shire'])) { + $consumerURL = (string)$_REQUEST['shire']; + } else { + $consumerURL = null; + } + + $requestId = null; + $IDPList = []; + $ProxyCount = null; + $RequesterID = null; + $forceAuthn = false; + $isPassive = false; + $consumerIndex = null; + $extensions = null; + $allowCreate = true; + $authnContext = null; + + $idpInit = true; + + Logger::info( + 'SAML2.0 - IdP.SSOService: IdP initiated authentication: ' . var_export($spEntityId, true) + ); + } else { + try { + $binding = Binding::getCurrentBinding(); + } catch (Exception $e) { + header($_SERVER["SERVER_PROTOCOL"]." 405 Method Not Allowed", true, 405); + exit; + } + $request = $binding->receive(); + + if (!($request instanceof AuthnRequest)) { + throw new Error\BadRequest( + 'Message received on authentication request endpoint wasn\'t an authentication request.' + ); + } + + /** @psalm-var null|string|\SAML2\XML\saml\Issuer $issuer Remove in SSP 2.0 */ + $issuer = $request->getIssuer(); + if ($issuer === null) { + throw new Error\BadRequest( + 'Received message on authentication request endpoint without issuer.' + ); + } elseif ($issuer instanceof Issuer) { + /** @psalm-var string|null $spEntityId */ + $spEntityId = $issuer->getValue(); + if ($spEntityId === null) { + /* Without an issuer we have no way to respond to the message. */ + throw new Error\BadRequest('Received message on logout endpoint without issuer.'); + } + } else { // we got a string, old case + $spEntityId = $issuer; + } + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + \SimpleSAML\Module\saml\Message::validateMessage($spMetadata, $idpMetadata, $request); + + $relayState = $request->getRelayState(); + + $requestId = $request->getId(); + $IDPList = $request->getIDPList(); + $ProxyCount = $request->getProxyCount(); + if ($ProxyCount !== null) { + $ProxyCount--; + } + $RequesterID = $request->getRequesterID(); + $forceAuthn = $request->getForceAuthn(); + $isPassive = $request->getIsPassive(); + $consumerURL = $request->getAssertionConsumerServiceURL(); + $protocolBinding = $request->getProtocolBinding(); + $consumerIndex = $request->getAssertionConsumerServiceIndex(); + $extensions = $request->getExtensions(); + $authnContext = $request->getRequestedAuthnContext(); + + $nameIdPolicy = $request->getNameIdPolicy(); + if (isset($nameIdPolicy['Format'])) { + $nameIDFormat = $nameIdPolicy['Format']; + } else { + $nameIDFormat = null; + } + if (isset($nameIdPolicy['AllowCreate'])) { + $allowCreate = $nameIdPolicy['AllowCreate']; + } else { + $allowCreate = false; + } + + $idpInit = false; + + Logger::info( + 'SAML2.0 - IdP.SSOService: incoming authentication request: ' . var_export($spEntityId, true) + ); + } + + Stats::log('saml:idp:AuthnRequest', [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'forceAuthn' => $forceAuthn, + 'isPassive' => $isPassive, + 'protocol' => 'saml2', + 'idpInit' => $idpInit, + ]); + + $acsEndpoint = self::getAssertionConsumerService( + $supportedBindings, + $spMetadata, + $consumerURL, + $protocolBinding, + $consumerIndex + ); + if ($acsEndpoint === null) { + throw new Exception('Unable to use any of the ACS endpoints found for SP \'' . $spEntityId . '\''); + } + + $IDPList = array_unique(array_merge($IDPList, $spMetadata->getArrayizeString('IDPList', []))); + if ($ProxyCount === null) { + $ProxyCount = $spMetadata->getInteger('ProxyCount', null); + } + + if (!$forceAuthn) { + $forceAuthn = $spMetadata->getBoolean('ForceAuthn', false); + } + + $sessionLostParams = [ + 'spentityid' => $spEntityId, + ]; + if ($relayState !== null) { + $sessionLostParams['RelayState'] = $relayState; + } + /* + Putting cookieTime as the last parameter makes unit testing easier since we don't need to handle a + changing time component in the middle of the url + */ + $sessionLostParams['cookieTime'] = time(); + + $sessionLostURL = Utils\HTTP::addURLParameters( + Utils\HTTP::getSelfURLNoQuery(), + $sessionLostParams + ); + + + /* + * Added by GTIS. + * This code is intended to ensure that a session from a new SP + * will be forced to reauthenticate if that SP is not allowed + * to authenticate through any of the IDP's that have so far + * been used for authentication. + * + * In order for this for this to avoid forcing authentication + * in every case, the hub's saml20-idp-hosted.php entry needs + * to include an authproc entry that adds each authenticating + * IDP to a list in the session. + * That list should be found in ... + * sessionDataType: 'sildisco:authentication' + * sessionKey: 'authenticated_idps' + * + * Another feature is that it forces the user to the discovery page, + * if the SP is allowed to use more than one IDP. The reason for this + * is that we want the user to be able to pick which of his ID's + * to use for this session. The way it is carried out is by expiring + * the user's session on the hub. (It does not log the user out from + * any of the IDP's.) + * + */ + $session = \SimpleSAML\Session::getSessionFromRequest(); + $sessionDataType = 'sildisco:authentication'; + $spIdKey = 'spentityid'; + $session->setData($sessionDataType, $spIdKey, $spEntityId); + $metadataPath = __DIR__ . '/../../../../metadata'; + $IDPList = array_keys(DiscoUtils::getIdpsForSp($spEntityId, $metadataPath)); + if ( ! $forceAuthn ) { + $sessionDataType = 'sildisco:authentication'; + $sessionKey = 'authenticated_idps'; + $authenticatedIdps = $session->getData($sessionDataType, $sessionKey); + + if ($authenticatedIdps) { + $metadataPath = __DIR__ . '/../../../../metadata/'; + $allowedIdps = DiscoUtils::getReducedIdpList( + $authenticatedIdps, + $metadataPath, + $spEntityId + ); + if ( ! $allowedIdps) { + $IDPList = Null; + $forceAuthn = True; + } + } else { // If there are no authenticated IDPs + $forceAuthn = True; + } + } + /* + * End of GTIS addition + */ + + $state = [ + 'Responder' => ['\SimpleSAML\Module\saml\IdP\SAML2', 'sendResponse'], + Auth\State::EXCEPTION_HANDLER_FUNC => [ + '\SimpleSAML\Module\saml\IdP\SAML2', + 'handleAuthError' + ], + Auth\State::RESTART => $sessionLostURL, + + 'SPMetadata' => $spMetadata->toArray(), + 'saml:RelayState' => $relayState, + 'saml:RequestId' => $requestId, + 'saml:IDPList' => $IDPList, + 'saml:ProxyCount' => $ProxyCount, + 'saml:RequesterID' => $RequesterID, + 'ForceAuthn' => $forceAuthn, + 'isPassive' => $isPassive, + 'saml:ConsumerURL' => $acsEndpoint['Location'], + 'saml:Binding' => $acsEndpoint['Binding'], + 'saml:NameIDFormat' => $nameIDFormat, + 'saml:AllowCreate' => $allowCreate, + 'saml:Extensions' => $extensions, + 'saml:AuthnRequestReceivedAt' => microtime(true), + 'saml:RequestedAuthnContext' => $authnContext, + ]; + + $idp->handleAuthenticationRequest($state); + } + + + /** + * Send a logout request to a given association. + * + * @param \SimpleSAML\IdP $idp The IdP we are sending a logout request from. + * @param array $association The association that should be terminated. + * @param string|null $relayState An id that should be carried across the logout. + * @return void + */ + public static function sendLogoutRequest(IdP $idp, array $association, $relayState) + { + assert(is_string($relayState) || $relayState === null); + + Logger::info('Sending SAML 2.0 LogoutRequest to: ' . var_export($association['saml:entityID'], true)); + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); + + Stats::log('saml:idp:LogoutRequest:sent', [ + 'spEntityID' => $association['saml:entityID'], + 'idpEntityID' => $idpMetadata->getString('entityid'), + ]); + + /** @var array $dst */ + $dst = $spMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ] + ); + $binding = Binding::getBinding($dst['Binding']); + $lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState); + $lr->setDestination($dst['Location']); + + $binding->send($lr); + } + + + /** + * Send a logout response. + * + * @param \SimpleSAML\IdP $idp The IdP we are sending a logout request from. + * @param array &$state The logout state array. + * @return void + */ + public static function sendLogoutResponse(IdP $idp, array $state) + { + assert(isset($state['saml:SPEntityId'])); + assert(isset($state['saml:RequestId'])); + assert(array_key_exists('saml:RelayState', $state)); // Can be NULL. + + $spEntityId = $state['saml:SPEntityId']; + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + $lr = \SimpleSAML\Module\saml\Message::buildLogoutResponse($idpMetadata, $spMetadata); + $lr->setInResponseTo($state['saml:RequestId']); + $lr->setRelayState($state['saml:RelayState']); + + if (isset($state['core:Failed']) && $state['core:Failed']) { + $partial = true; + $lr->setStatus([ + 'Code' => Constants::STATUS_SUCCESS, + 'SubCode' => Constants::STATUS_PARTIAL_LOGOUT, + ]); + Logger::info('Sending logout response for partial logout to SP ' . var_export($spEntityId, true)); + } else { + $partial = false; + Logger::debug('Sending logout response to SP ' . var_export($spEntityId, true)); + } + + Stats::log('saml:idp:LogoutResponse:sent', [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + 'partial' => $partial + ]); + + /** @var array $dst */ + $dst = $spMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ] + ); + $binding = Binding::getBinding($dst['Binding']); + if (isset($dst['ResponseLocation'])) { + $dst = $dst['ResponseLocation']; + } else { + $dst = $dst['Location']; + } + $lr->setDestination($dst); + + $binding->send($lr); + } + + + /** + * Receive a logout message. + * + * @param \SimpleSAML\IdP $idp The IdP we are receiving it for. + * @return void + * @throws \SimpleSAML\Error\BadRequest In case an error occurs while trying to receive the logout message. + */ + public static function receiveLogoutMessage(IdP $idp) + { + $binding = Binding::getCurrentBinding(); + $message = $binding->receive(); + + /** @psalm-var null|string|\SAML2\XML\saml\Issuer Remove in SSP 2.0 */ + $issuer = $message->getIssuer(); + if ($issuer === null) { + /* Without an issuer we have no way to respond to the message. */ + throw new Error\BadRequest('Received message on logout endpoint without issuer.'); + } elseif ($issuer instanceof Issuer) { + /** @psalm-var string|null $spEntityId */ + $spEntityId = $issuer->getValue(); + if ($spEntityId === null) { + /* Without an issuer we have no way to respond to the message. */ + throw new Error\BadRequest('Received message on logout endpoint without issuer.'); + } + } else { + $spEntityId = $issuer; + } + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); + + \SimpleSAML\Module\saml\Message::validateMessage($spMetadata, $idpMetadata, $message); + + if ($message instanceof LogoutResponse) { + Logger::info('Received SAML 2.0 LogoutResponse from: ' . var_export($spEntityId, true)); + $statsData = [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + ]; + if (!$message->isSuccess()) { + $statsData['error'] = $message->getStatus(); + } + Stats::log('saml:idp:LogoutResponse:recv', $statsData); + + $relayState = $message->getRelayState(); + + if (!$message->isSuccess()) { + $logoutError = \SimpleSAML\Module\saml\Message::getResponseError($message); + Logger::warning('Unsuccessful logout. Status was: ' . $logoutError); + } else { + $logoutError = null; + } + + $assocId = 'saml:' . $spEntityId; + + $idp->handleLogoutResponse($assocId, $relayState, $logoutError); + } elseif ($message instanceof LogoutRequest) { + Logger::info('Received SAML 2.0 LogoutRequest from: ' . var_export($spEntityId, true)); + Stats::log('saml:idp:LogoutRequest:recv', [ + 'spEntityID' => $spEntityId, + 'idpEntityID' => $idpMetadata->getString('entityid'), + ]); + + $spStatsId = $spMetadata->getString('core:statistics-id', $spEntityId); + Logger::stats('saml20-idp-SLO spinit ' . $spStatsId . ' ' . $idpMetadata->getString('entityid')); + + $state = [ + 'Responder' => ['\SimpleSAML\Module\saml\IdP\SAML2', 'sendLogoutResponse'], + 'saml:SPEntityId' => $spEntityId, + 'saml:RelayState' => $message->getRelayState(), + 'saml:RequestId' => $message->getId(), + ]; + + $assocId = 'saml:' . $spEntityId; + $idp->handleLogoutRequest($state, $assocId); + } else { + throw new Error\BadRequest('Unknown message received on logout endpoint: ' . get_class($message)); + } + } + + + /** + * Retrieve a logout URL for a given logout association. + * + * @param \SimpleSAML\IdP $idp The IdP we are sending a logout request from. + * @param array $association The association that should be terminated. + * @param string|NULL $relayState An id that should be carried across the logout. + * + * @return string The logout URL. + */ + public static function getLogoutURL(IdP $idp, array $association, $relayState) + { + assert(is_string($relayState) || $relayState === null); + + Logger::info('Sending SAML 2.0 LogoutRequest to: ' . var_export($association['saml:entityID'], true)); + + $metadata = MetaDataStorageHandler::getMetadataHandler(); + $idpMetadata = $idp->getConfig(); + $spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); + + $bindings = [ + Constants::BINDING_HTTP_REDIRECT, + Constants::BINDING_HTTP_POST + ]; + + /** @var array $dst */ + $dst = $spMetadata->getEndpointPrioritizedByBinding('SingleLogoutService', $bindings); + + if ($dst['Binding'] === Constants::BINDING_HTTP_POST) { + $params = ['association' => $association['id'], 'idp' => $idp->getId()]; + if ($relayState !== null) { + $params['RelayState'] = $relayState; + } + return Module::getModuleURL('core/idp/logout-iframe-post.php', $params); + } + + $lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState); + $lr->setDestination($dst['Location']); + + $binding = new HTTPRedirect(); + return $binding->getRedirectURL($lr); + } + + + /** + * Retrieve the metadata for the given SP association. + * + * @param \SimpleSAML\IdP $idp The IdP the association belongs to. + * @param array $association The SP association. + * + * @return \SimpleSAML\Configuration Configuration object for the SP metadata. + */ + public static function getAssociationConfig(IdP $idp, array $association) + { + $metadata = MetaDataStorageHandler::getMetadataHandler(); + try { + return $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); + } catch (Exception $e) { + return Configuration::loadFromArray([], 'Unknown SAML 2 entity.'); + } + } + + + /** + * Retrieve the metadata of a hosted SAML 2 IdP. + * + * @param string $entityid The entity ID of the hosted SAML 2 IdP whose metadata we want. + * + * @return array + * @throws \SimpleSAML\Error\CriticalConfigurationError + * @throws \SimpleSAML\Error\Exception + * @throws \SimpleSAML\Error\MetadataNotFound + */ + public static function getHostedMetadata($entityid) + { + $handler = MetaDataStorageHandler::getMetadataHandler(); + $config = $handler->getMetaDataConfig($entityid, 'saml20-idp-hosted'); + + // configure endpoints + $ssob = $handler->getGenerated('SingleSignOnServiceBinding', 'saml20-idp-hosted'); + $slob = $handler->getGenerated('SingleLogoutServiceBinding', 'saml20-idp-hosted'); + $ssol = $handler->getGenerated('SingleSignOnService', 'saml20-idp-hosted'); + $slol = $handler->getGenerated('SingleLogoutService', 'saml20-idp-hosted'); + + $sso = []; + if (is_array($ssob)) { + foreach ($ssob as $binding) { + $sso[] = [ + 'Binding' => $binding, + 'Location' => $ssol, + ]; + } + } else { + $sso[] = [ + 'Binding' => $ssob, + 'Location' => $ssol, + ]; + } + + $slo = []; + if (is_array($slob)) { + foreach ($slob as $binding) { + $slo[] = [ + 'Binding' => $binding, + 'Location' => $slol, + ]; + } + } else { + $slo[] = [ + 'Binding' => $slob, + 'Location' => $slol, + ]; + } + + $metadata = [ + 'metadata-set' => 'saml20-idp-hosted', + 'entityid' => $entityid, + 'SingleSignOnService' => $sso, + 'SingleLogoutService' => $slo, + 'NameIDFormat' => $config->getArrayizeString('NameIDFormat', Constants::NAMEID_TRANSIENT), + ]; + + // add certificates + $keys = []; + $certInfo = Utils\Crypto::loadPublicKey($config, false, 'new_'); + $hasNewCert = false; + if ($certInfo !== null) { + $keys[] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => true, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => 'new_', + ]; + $hasNewCert = true; + } + + /** @var array $certInfo */ + $certInfo = Utils\Crypto::loadPublicKey($config, true); + $keys[] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => $hasNewCert === false, + 'X509Certificate' => $certInfo['certData'], + 'prefix' => '', + ]; + + if ($config->hasValue('https.certificate')) { + /** @var array $httpsCert */ + $httpsCert = Utils\Crypto::loadPublicKey($config, true, 'https.'); + $keys[] = [ + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => false, + 'X509Certificate' => $httpsCert['certData'], + 'prefix' => 'https.' + ]; + } + $metadata['keys'] = $keys; + + // add ArtifactResolutionService endpoint, if enabled + if ($config->getBoolean('saml20.sendartifact', false)) { + $metadata['ArtifactResolutionService'][] = [ + 'index' => 0, + 'Binding' => Constants::BINDING_SOAP, + 'Location' => Utils\HTTP::getBaseURL() . 'saml2/idp/ArtifactResolutionService.php' + ]; + } + + // add Holder of Key, if enabled + if ($config->getBoolean('saml20.hok.assertion', false)) { + array_unshift( + $metadata['SingleSignOnService'], + [ + 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => Constants::BINDING_HOK_SSO, + 'Location' => Utils\HTTP::getBaseURL() . 'saml2/idp/SSOService.php', + ] + ); + } + + // add ECP profile, if enabled + if ($config->getBoolean('saml20.ecp', false)) { + $metadata['SingleSignOnService'][] = [ + 'index' => 0, + 'Binding' => Constants::BINDING_SOAP, + 'Location' => Utils\HTTP::getBaseURL() . 'saml2/idp/SSOService.php', + ]; + } + + // add organization information + if ($config->hasValue('OrganizationName')) { + $metadata['OrganizationName'] = $config->getLocalizedString('OrganizationName'); + $metadata['OrganizationDisplayName'] = $config->getLocalizedString( + 'OrganizationDisplayName', + $metadata['OrganizationName'] + ); + + if (!$config->hasValue('OrganizationURL')) { + throw new Error\Exception('If OrganizationName is set, OrganizationURL must also be set.'); + } + $metadata['OrganizationURL'] = $config->getLocalizedString('OrganizationURL'); + } + + // add scope + if ($config->hasValue('scope')) { + $metadata['scope'] = $config->getArray('scope'); + } + + // add extensions + if ($config->hasValue('EntityAttributes')) { + $metadata['EntityAttributes'] = $config->getArray('EntityAttributes'); + + // check for entity categories + if (Utils\Config\Metadata::isHiddenFromDiscovery($metadata)) { + $metadata['hide.from.discovery'] = true; + } + } + + if ($config->hasValue('UIInfo')) { + $metadata['UIInfo'] = $config->getArray('UIInfo'); + } + + if ($config->hasValue('DiscoHints')) { + $metadata['DiscoHints'] = $config->getArray('DiscoHints'); + } + + if ($config->hasValue('RegistrationInfo')) { + $metadata['RegistrationInfo'] = $config->getArray('RegistrationInfo'); + } + + // configure signature options + if ($config->hasValue('validate.authnrequest')) { + $metadata['sign.authnrequest'] = $config->getBoolean('validate.authnrequest'); + } + + if ($config->hasValue('redirect.validate')) { + $metadata['redirect.sign'] = $config->getBoolean('redirect.validate'); + } + + // add contact information + if ($config->hasValue('contacts')) { + $contacts = $config->getArray('contacts'); + foreach ($contacts as $contact) { + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + } + + $globalConfig = Configuration::getInstance(); + $email = $globalConfig->getString('technicalcontact_email', false); + if ($email && $email !== 'na@example.org') { + $contact = [ + 'emailAddress' => $email, + 'name' => $globalConfig->getString('technicalcontact_name', null), + 'contactType' => 'technical', + ]; + $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact); + } + + return $metadata; + } + + + /** + * Calculate the NameID value that should be used. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array &$state The authentication state of the user. + * + * @return string|null The NameID value. + */ + private static function generateNameIdValue( + Configuration $idpMetadata, + Configuration $spMetadata, + array &$state + ): ?string { + $attribute = $spMetadata->getString('simplesaml.nameidattribute', null); + if ($attribute === null) { + $attribute = $idpMetadata->getString('simplesaml.nameidattribute', null); + if ($attribute === null) { + if (!isset($state['UserID'])) { + Logger::error('Unable to generate NameID. Check the userid.attribute option.'); + return null; + } + $attributeValue = $state['UserID']; + $idpEntityId = $idpMetadata->getString('entityid'); + $spEntityId = $spMetadata->getString('entityid'); + + $secretSalt = Utils\Config::getSecretSalt(); + + $uidData = 'uidhashbase' . $secretSalt; + $uidData .= strlen($idpEntityId) . ':' . $idpEntityId; + $uidData .= strlen($spEntityId) . ':' . $spEntityId; + $uidData .= strlen($attributeValue) . ':' . $attributeValue; + $uidData .= $secretSalt; + + return hash('sha1', $uidData); + } + } + + $attributes = $state['Attributes']; + if (!array_key_exists($attribute, $attributes)) { + Logger::error('Unable to add NameID: Missing ' . var_export($attribute, true) . + ' in the attributes of the user.'); + return null; + } + + return $attributes[$attribute][0]; + } + + + /** + * Helper function for encoding attributes. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array $attributes The attributes of the user. + * + * @return array The encoded attributes. + * + * @throws \SimpleSAML\Error\Exception In case an unsupported encoding is specified by configuration. + */ + private static function encodeAttributes( + Configuration $idpMetadata, + Configuration $spMetadata, + array $attributes + ): array { + $base64Attributes = $spMetadata->getBoolean('base64attributes', null); + if ($base64Attributes === null) { + $base64Attributes = $idpMetadata->getBoolean('base64attributes', false); + } + + if ($base64Attributes) { + $defaultEncoding = 'base64'; + } else { + $defaultEncoding = 'string'; + } + + $srcEncodings = $idpMetadata->getArray('attributeencodings', []); + $dstEncodings = $spMetadata->getArray('attributeencodings', []); + + /* + * Merge the two encoding arrays. Encodings specified in the target metadata + * takes precedence over the source metadata. + */ + $encodings = array_merge($srcEncodings, $dstEncodings); + + $ret = []; + foreach ($attributes as $name => $values) { + $ret[$name] = []; + if (array_key_exists($name, $encodings)) { + $encoding = $encodings[$name]; + } else { + $encoding = $defaultEncoding; + } + + foreach ($values as $value) { + // allow null values + if ($value === null) { + $ret[$name][] = $value; + continue; + } + + $attrval = $value; + if ($value instanceof DOMNodeList) { + /** @psalm-suppress PossiblyNullPropertyFetch */ + $attrval = new AttributeValue($value->item(0)->parentNode); + } + + switch ($encoding) { + case 'string': + $value = (string) $attrval; + break; + case 'base64': + $value = base64_encode((string) $attrval); + break; + case 'raw': + if (is_string($value)) { + $doc = DOMDocumentFactory::fromString('' . $value . ''); + $value = $doc->firstChild->childNodes; + } + assert($value instanceof DOMNodeList || $value instanceof NameID); + break; + default: + throw new Error\Exception('Invalid encoding for attribute ' . + var_export($name, true) . ': ' . var_export($encoding, true)); + } + $ret[$name][] = $value; + } + } + + return $ret; + } + + + /** + * Determine which NameFormat we should use for attributes. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * + * @return string The NameFormat. + */ + private static function getAttributeNameFormat( + Configuration $idpMetadata, + Configuration $spMetadata + ): string { + // try SP metadata first + $attributeNameFormat = $spMetadata->getString('attributes.NameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + $attributeNameFormat = $spMetadata->getString('AttributeNameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + + // look in IdP metadata + $attributeNameFormat = $idpMetadata->getString('attributes.NameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + $attributeNameFormat = $idpMetadata->getString('AttributeNameFormat', null); + if ($attributeNameFormat !== null) { + return $attributeNameFormat; + } + + // default + return Constants::NAMEFORMAT_BASIC; + } + + + /** + * Build an assertion based on information in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array &$state The state array with information about the request. + * + * @return \SAML2\Assertion The assertion. + * + * @throws \SimpleSAML\Error\Exception In case an error occurs when creating a holder-of-key assertion. + */ + private static function buildAssertion( + Configuration $idpMetadata, + Configuration $spMetadata, + array &$state + ): Assertion { + assert(isset($state['Attributes'])); + assert(isset($state['saml:ConsumerURL'])); + + $now = time(); + + $signAssertion = $spMetadata->getBoolean('saml20.sign.assertion', null); + if ($signAssertion === null) { + $signAssertion = $idpMetadata->getBoolean('saml20.sign.assertion', true); + } + + $config = Configuration::getInstance(); + + $a = new Assertion(); + if ($signAssertion) { + \SimpleSAML\Module\saml\Message::addSign($idpMetadata, $spMetadata, $a); + } + + $issuer = new Issuer(); + $issuer->setValue($idpMetadata->getString('entityid')); + $issuer->setFormat(Constants::NAMEID_ENTITY); + $a->setIssuer($issuer); + + $audience = array_merge([$spMetadata->getString('entityid')], $spMetadata->getArray('audience', [])); + $a->setValidAudiences($audience); + + $a->setNotBefore($now - 30); + + $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', null); + if ($assertionLifetime === null) { + $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300); + } + $a->setNotOnOrAfter($now + $assertionLifetime); + + if (isset($state['saml:AuthnContextClassRef'])) { + $a->setAuthnContextClassRef($state['saml:AuthnContextClassRef']); + } elseif (Utils\HTTP::isHTTPS()) { + $a->setAuthnContextClassRef(Constants::AC_PASSWORD_PROTECTED_TRANSPORT); + } else { + $a->setAuthnContextClassRef(Constants::AC_PASSWORD); + } + + $sessionStart = $now; + if (isset($state['AuthnInstant'])) { + $a->setAuthnInstant($state['AuthnInstant']); + $sessionStart = $state['AuthnInstant']; + } + + $sessionLifetime = $config->getInteger('session.duration', 8 * 60 * 60); + $a->setSessionNotOnOrAfter($sessionStart + $sessionLifetime); + + $a->setSessionIndex(Utils\Random::generateID()); + + $sc = new SubjectConfirmation(); + $scd = new SubjectConfirmationData(); + $scd->setNotOnOrAfter($now + $assertionLifetime); + $scd->setRecipient($state['saml:ConsumerURL']); + $scd->setInResponseTo($state['saml:RequestId']); + $sc->setSubjectConfirmationData($scd); + + // ProtcolBinding of SP's overwrites IdP hosted metadata configuration + $hokAssertion = null; + if ($state['saml:Binding'] === Constants::BINDING_HOK_SSO) { + $hokAssertion = true; + } + if ($hokAssertion === null) { + $hokAssertion = $idpMetadata->getBoolean('saml20.hok.assertion', false); + } + + if ($hokAssertion) { + // Holder-of-Key + $sc->setMethod(Constants::CM_HOK); + if (Utils\HTTP::isHTTPS()) { + if (isset($_SERVER['SSL_CLIENT_CERT']) && !empty($_SERVER['SSL_CLIENT_CERT'])) { + // extract certificate data (if this is a certificate) + $clientCert = $_SERVER['SSL_CLIENT_CERT']; + $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; + if (preg_match($pattern, $clientCert, $matches)) { + // we have a client certificate from the browser which we add to the HoK assertion + $x509Certificate = new X509Certificate(); + $x509Certificate->setCertificate(str_replace(["\r", "\n", " "], '', $matches[1])); + + $x509Data = new X509Data(); + $x509Data->addData($x509Certificate); + + $keyInfo = new KeyInfo(); + $keyInfo->addInfo($x509Data); + + $scd->addInfo($keyInfo); + } else { + throw new Error\Exception( + 'Error creating HoK assertion: No valid client certificate provided during ' + . 'TLS handshake with IdP' + ); + } + } else { + throw new Error\Exception( + 'Error creating HoK assertion: No client certificate provided during TLS handshake with IdP' + ); + } + } else { + throw new Error\Exception( + 'Error creating HoK assertion: No HTTPS connection to IdP, but required for Holder-of-Key SSO' + ); + } + } else { + // Bearer + $sc->setMethod(Constants::CM_BEARER); + } + $sc->setSubjectConfirmationData($scd); + $a->setSubjectConfirmation([$sc]); + + // add attributes + if ($spMetadata->getBoolean('simplesaml.attributes', true)) { + $attributeNameFormat = self::getAttributeNameFormat($idpMetadata, $spMetadata); + $a->setAttributeNameFormat($attributeNameFormat); + $attributes = self::encodeAttributes($idpMetadata, $spMetadata, $state['Attributes']); + $a->setAttributes($attributes); + } + + $nameIdFormat = null; + + // generate the NameID for the assertion + if (isset($state['saml:NameIDFormat'])) { + $nameIdFormat = $state['saml:NameIDFormat']; + } + + if ($nameIdFormat === null || !isset($state['saml:NameID'][$nameIdFormat])) { + // either not set in request, or not set to a format we supply. Fall back to old generation method + $nameIdFormat = current($spMetadata->getArrayizeString('NameIDFormat', [])); + if ($nameIdFormat === false) { + $nameIdFormat = current($idpMetadata->getArrayizeString('NameIDFormat', [Constants::NAMEID_TRANSIENT])); + } + } + + if (isset($state['saml:NameID'][$nameIdFormat])) { + $nameId = $state['saml:NameID'][$nameIdFormat]; + $nameId->setFormat($nameIdFormat); + } else { + $spNameQualifier = $spMetadata->getString('SPNameQualifier', null); + if ($spNameQualifier === null) { + $spNameQualifier = $spMetadata->getString('entityid'); + } + + if ($nameIdFormat === Constants::NAMEID_TRANSIENT) { + // generate a random id + $nameIdValue = Utils\Random::generateID(); + } else { + /* this code will end up generating either a fixed assigned id (via nameid.attribute) + or random id if not assigned/configured */ + $nameIdValue = self::generateNameIdValue($idpMetadata, $spMetadata, $state); + if ($nameIdValue === null) { + Logger::warning('Falling back to transient NameID.'); + $nameIdFormat = Constants::NAMEID_TRANSIENT; + $nameIdValue = Utils\Random::generateID(); + } + } + + $nameId = new NameID(); + $nameId->setFormat($nameIdFormat); + $nameId->setValue($nameIdValue); + $nameId->setSPNameQualifier($spNameQualifier); + } + + $state['saml:idp:NameID'] = $nameId; + + $a->setNameId($nameId); + + $encryptNameId = $spMetadata->getBoolean('nameid.encryption', null); + if ($encryptNameId === null) { + $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', false); + } + if ($encryptNameId) { + $a->encryptNameId(\SimpleSAML\Module\saml\Message::getEncryptionKey($spMetadata)); + } + + return $a; + } + + + /** + * Encrypt an assertion. + * + * This function takes in a \SAML2\Assertion and encrypts it if encryption of + * assertions are enabled in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param \SAML2\Assertion $assertion The assertion we are encrypting. + * + * @return \SAML2\Assertion|\SAML2\EncryptedAssertion The assertion. + * + * @throws \SimpleSAML\Error\Exception In case the encryption key type is not supported. + */ + private static function encryptAssertion( + Configuration $idpMetadata, + Configuration $spMetadata, + Assertion $assertion + ) { + $encryptAssertion = $spMetadata->getBoolean('assertion.encryption', null); + if ($encryptAssertion === null) { + $encryptAssertion = $idpMetadata->getBoolean('assertion.encryption', false); + } + if (!$encryptAssertion) { + // we are _not_ encrypting this assertion, and are therefore done + return $assertion; + } + + + $sharedKey = $spMetadata->getString('sharedkey', null); + if ($sharedKey !== null) { + $algo = $spMetadata->getString('sharedkey_algorithm', null); + if ($algo === null) { + $algo = $idpMetadata->getString('sharedkey_algorithm'); + } + + $key = new XMLSecurityKey($algo); + $key->loadKey($sharedKey); + } else { + $keys = $spMetadata->getPublicKeys('encryption', true); + if (!empty($keys)) { + $key = $keys[0]; + switch ($key['type']) { + case 'X509Certificate': + $pemKey = "-----BEGIN CERTIFICATE-----\n" . + chunk_split($key['X509Certificate'], 64) . + "-----END CERTIFICATE-----\n"; + break; + default: + throw new Error\Exception('Unsupported encryption key type: ' . $key['type']); + } + + // extract the public key from the certificate for encryption + $key = new XMLSecurityKey(XMLSecurityKey::RSA_OAEP_MGF1P, ['type' => 'public']); + $key->loadKey($pemKey); + } else { + throw new Error\ConfigurationError( + 'Missing encryption key for entity `' . $spMetadata->getString('entityid') . '`', + $spMetadata->getString('metadata-set') . '.php', + null + ); + } + } + + $ea = new EncryptedAssertion(); + $ea->setAssertion($assertion, $key); + return $ea; + } + + + /** + * Build a logout request based on information in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param array $association The SP association. + * @param string|null $relayState An id that should be carried across the logout. + * + * @return \SAML2\LogoutRequest The corresponding SAML2 logout request. + */ + private static function buildLogoutRequest( + Configuration $idpMetadata, + Configuration $spMetadata, + array $association, + string $relayState = null + ): LogoutRequest { + $lr = \SimpleSAML\Module\saml\Message::buildLogoutRequest($idpMetadata, $spMetadata); + $lr->setRelayState($relayState); + $lr->setSessionIndex($association['saml:SessionIndex']); + $lr->setNameId($association['saml:NameID']); + + $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', null); + if ($assertionLifetime === null) { + $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300); + } + $lr->setNotOnOrAfter(time() + $assertionLifetime); + + $encryptNameId = $spMetadata->getBoolean('nameid.encryption', null); + if ($encryptNameId === null) { + $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', false); + } + if ($encryptNameId) { + $lr->encryptNameId(\SimpleSAML\Module\saml\Message::getEncryptionKey($spMetadata)); + } + + return $lr; + } + + + /** + * Build a authentication response based on information in the metadata. + * + * @param \SimpleSAML\Configuration $idpMetadata The metadata of the IdP. + * @param \SimpleSAML\Configuration $spMetadata The metadata of the SP. + * @param string $consumerURL The Destination URL of the response. + * + * @return \SAML2\Response The SAML2 Response corresponding to the given data. + */ + private static function buildResponse( + Configuration $idpMetadata, + Configuration $spMetadata, + string $consumerURL + ): Response { + $signResponse = $spMetadata->getBoolean('saml20.sign.response', null); + if ($signResponse === null) { + $signResponse = $idpMetadata->getBoolean('saml20.sign.response', true); + } + + $r = new Response(); + $issuer = new Issuer(); + $issuer->setValue($idpMetadata->getString('entityid')); + $issuer->setFormat(Constants::NAMEID_ENTITY); + $r->setIssuer($issuer); + $r->setDestination($consumerURL); + + if ($signResponse) { + \SimpleSAML\Module\saml\Message::addSign($idpMetadata, $spMetadata, $r); + } + + return $r; + } +} \ No newline at end of file diff --git a/modules/sildisco/lib/IdPDisco.php b/modules/sildisco/lib/IdPDisco.php new file mode 100644 index 00000000..0ea08a17 --- /dev/null +++ b/modules/sildisco/lib/IdPDisco.php @@ -0,0 +1,214 @@ +session->getData($sessionDataType, $sessionKeyForSP); */ + public static $sessionDataType = 'sildisco:authentication'; + public static $sessionKeyForSP = 'spentityid'; + + + /** + * Log a message. + * + * This is an helper function for logging messages. It will prefix the messages with our discovery service type. + * + * @param string $message The message which should be logged. + */ + protected function log($message) + { + \SimpleSAML\Logger::info('SildiscoIdPDisco.'.$this->instance.': '.$message); + } + + /* Path to the folder with the SP and IdP metadata */ + private function getMetadataPath() { + return __DIR__ . '/../../../metadata/'; + } + + private function getSPEntityIDAndReducedIdpList() + { + + $idpList = $this->getIdPList(); + $idpList = $this->filterList($idpList); + + $spEntityId = $this->session->getData(self::$sessionDataType, self::$sessionKeyForSP); + + $idpList = DiscoUtils::getReducedIdpList( + $idpList, + $this->getMetadataPath(), + $spEntityId + ); + + return array($spEntityId, self::enableBetaEnabled($idpList)); + } + + /** + * Handles a request to this discovery service. + * + * The IdP disco parameters should be set before calling this function. + */ + public function handleRequest() + { + + $this->start(); + list($spEntityId, $idpList) = $this->getSPEntityIDAndReducedIdpList(); + + if (sizeof($idpList) == 1) { + $idp = array_keys($idpList)[0]; + $idp = $this->validateIdP($idp); + if ($idp !== null) { + + $this->log( + 'Choice made [' . $idp . '] (Redirecting the user back. returnIDParam=' . + $this->returnIdParam . ')' + ); + + \SimpleSAML\Utils\HTTP::redirectTrustedURL( + $this->returnURL, + array($this->returnIdParam => $idp) + ); + } + } + + // Get the SP's name + $spEntries = Metadata::getSpMetadataEntries($this->getMetadataPath()); + + $t = new \SimpleSAML\XHTML\Template($this->config, 'selectidp-links.php', 'disco'); + + $spName = null; + + $rawSPName = $spEntries[$spEntityId][self::$spNameMdKey] ?? null; + if ($rawSPName !== null) { + $spName = htmlspecialchars($t->getTranslator()->getPreferredTranslation( + \SimpleSAML\Utils\Arrays::arrayize($rawSPName, 'en') + )) ; + } + + $t->data['idplist'] = $idpList; + $t->data['return'] = $this->returnURL; + $t->data['returnIDParam'] = $this->returnIdParam; + $t->data['entityID'] = $this->spEntityId; + $t->data['spName'] = $spName; + $t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery()); + $t->data['announcement'] = AnnouncementUtils::getSimpleAnnouncement(); + $t->data['helpCenterUrl'] = $this->config->getValue('helpCenterUrl', ''); + + $t->show(); + } + + /** + * @param array $idpList the IDPs with their metadata + * @param bool $isBetaTester optional (default=null) just for unit testing + * @return array $idpList + * + * If the current user has the beta_tester cookie, then for each IDP in + * the idpList that has 'betaEnabled' => true, give it 'enabled' => true + * + */ + public static function enableBetaEnabled($idpList, $isBetaTester=null) { + + if ( $isBetaTester === null) { + $session = \SimpleSAML\Session::getSessionFromRequest(); + $isBetaTester = $session->getData( + self::$sessionType, + self::$betaTesterSessionKey + ); + } + + if ( ! $isBetaTester) { + return $idpList; + } + + foreach ($idpList as $idp => $idpMetadata) { + if ( ! empty($idpMetadata[self::$betaEnabledMdKey])) { + $idpMetadata[self::$enabledMdKey] = true; + $idpList[$idp] = $idpMetadata; + } + } + + return $idpList; + } + + /** + * Validates the given IdP entity id. + * + * Takes a string with the IdP entity id, and returns the entity id if it is valid, or + * null if not. Ensures that the selected IdP is allowed for the current SP + * + * @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null. + * + * @return string|null The entity id if it is valid, null if not. + */ + protected function validateIdP($idp) + { + if ($idp === null) { + return null; + } + if (!$this->config->getBoolean('idpdisco.validate', true)) { + return $idp; + } + + list($spEntityId, $idpList) = $this->getSPEntityIDAndReducedIdpList(); + + /* + * All this complication is for security. + * Without it a user is able to use his authentication through an + * IdP to login to an SP that normally shouldn't accept that IdP. + * + * With a good process, the current SP's entity ID will appear in the + * session and in the request's 'return' entry. + * + * With a hacked process, the SP in the session will not appear in the + * request's 'return' entry. + */ + $returnKey = 'return'; + $requestReturn = array_key_exists($returnKey, $_REQUEST) ? + urldecode(urldecode($_REQUEST[$returnKey])) : ""; + + $spEntityIdParam = 'spentityid='.$spEntityId; + + if (strpos($requestReturn, $spEntityIdParam) === false) { + $message = 'Invalid SP entity id [' . $spEntityId . ']. ' . + 'Could not find in return value. ' . PHP_EOL . $requestReturn; + $this->log($message); + return null; + } + + + if (array_key_exists($idp, $idpList) && $idpList[$idp]['enabled']) { + return $idp; + } + $this->log('Invalid IdP entity id ['.$idp.'] received from discovery page.'); + // the entity id wasn't valid + return null; + } +} diff --git a/modules/sildisco/lib/SSOService.php b/modules/sildisco/lib/SSOService.php new file mode 100644 index 00000000..45d972dd --- /dev/null +++ b/modules/sildisco/lib/SSOService.php @@ -0,0 +1,47 @@ + + * @package SimpleSAMLphp + */ + +require_once('../../_include.php'); + +\SimpleSAML\Logger::info('SAML2.0 - IdP.SSOService: Accessing SAML 2.0 IdP endpoint SSOService'); + +$metadata = \SimpleSAML\Metadata\MetaDataStorageHandler::getMetadataHandler(); + +$config = \SimpleSAML\Configuration::getInstance(); +if (!$config->getBoolean('enable.saml20-idp', false) || !\SimpleSAML\Module::isModuleEnabled('saml')) { + throw new \SimpleSAML\Error\Error('NOACCESS', null, 403); +} + +$idpEntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted'); +$idp = \SimpleSAML\IdP::getById('saml2:' . $idpEntityId); + +$hubModeKey = 'hubmode'; + +try { +// If in hub mode, then use the sildisco entry script + if ($config->getValue($hubModeKey, false)) { + \SimpleSAML\Module\sildisco\IdP\SAML2::receiveAuthnRequest($idp); + } else { + \SimpleSAML\Module\saml\IdP\SAML2::receiveAuthnRequest($idp); + } +} catch (\Exception $e) { + if ($e->getMessage() === "Unable to find the current binding.") { + throw new \SimpleSAML\Error\Error('SSOPARAMS', $e, 400); + } else { + throw $e; // do not ignore other exceptions! + } +} +assert(false); diff --git a/modules/sildisco/tests/AddIdpTest.php b/modules/sildisco/tests/AddIdpTest.php new file mode 100644 index 00000000..40ffcd20 --- /dev/null +++ b/modules/sildisco/tests/AddIdpTest.php @@ -0,0 +1,128 @@ + $idp, + 'saml:sp:NameID' => [ + [ + 'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + 'Value' => 'Tester1_Smith', + 'SPNameQualifier' => 'http://ssp-sp1.local', + ], + ], + 'Attributes' => [], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + } + + /** + * Helper function to run the filter with a given configuration. + * + * @param array $config The filter configuration. + * @param array $request The request state. + * @return array The state array after processing. + */ + private static function processAddIdp2NameId(array $config, array $request) + { + $filter = new \SimpleSAML\Module\sildisco\Auth\Process\AddIdp2NameId($config, NULL); + $filter->process($request); + return $request; + } + + /* + * Test with IdP metadata not having an IDPNamespace entry + * @expectedException \SimpleSAML\Error\Exception + */ + public function testAddIdp2NameId_NoIDPNamespace() + { + $this->setExpectedException('\SimpleSAML\Error\Exception'); + $config = [ 'test' => ['value1', 'value2'], ]; + $request = self::getNameID('idp-bare'); + + self::processAddIdp2NameId($config, $request); + } + + + /* + * Test with IdP metadata not having an IDPNamespace entry + * @expectedException \SimpleSAML\Error\Exception + */ + public function testAddIdp2NameId_EmptyIDPNamespace() + { + $this->setExpectedException('\SimpleSAML\Error\Exception'); + $config = [ 'test' => ['value1', 'value2'], ]; + $request = self::getNameID('idp-empty'); + self::processAddIdp2NameId($config, $request); + } + + /* + * Test with IdP metadata not having an IDPNamespace entry + * @expectedException \SimpleSAML\Error\Exception + */ + public function testAddIdp2NameId_BadIDPNamespace() + { + $this->setExpectedException('\SimpleSAML\Error\Exception'); + $config = [ + 'test' => ['value1', 'value2'], + ]; + $request = self::getNameID('idp-bad'); + self::processAddIdp2NameId($config, $request); + } + + + + /* + * Test with IdP metadata having a good IDPNamespace entry + */ + public function testAddIdp2NameId_GoodString() + { + $config = ['test' => ['value1', 'value2']]; + $state = [ + 'saml:sp:IdP' => 'idp-good', + 'saml:sp:NameID' => 'Tester1_SmithA', + 'Attributes' => [], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $newNameID = $state['saml:sp:NameID']; + $newNameID = 'Tester1_SmithA@idpGood'; + + $expected = $state; + $expected['saml:NameID']['urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'] = $newNameID; + + $results = self::processAddIdp2NameId($config, $state); + $this->assertEquals($expected, $results); + } + /* + * Test with IdP metadata having a good IDPNamespace entry + */ + public function testAddIdp2NameId_GoodArray() + { + $config = ['test' => ['value1', 'value2']]; + $state = [ + 'saml:sp:IdP' => 'idp-good', + 'saml:sp:NameID' => [ + 'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:transient', + 'Value' => 'Tester1_SmithA', + 'SPNameQualifier' => 'http://ssp-sp1.local', + ], + 'Attributes' => [], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $newNameID = $state['saml:sp:NameID']; + $newNameID['Value'] = 'Tester1_SmithA@idpGood'; + + $expected = $state; + $expected['saml:NameID']['urn:oasis:names:tc:SAML:1.1:nameid-format:transient'] = $newNameID; + + $results = self::processAddIdp2NameId($config, $state); + + $this->assertEquals($expected, $results); + } + +} diff --git a/modules/sildisco/tests/TagGroupTest.php b/modules/sildisco/tests/TagGroupTest.php new file mode 100644 index 00000000..f00a601c --- /dev/null +++ b/modules/sildisco/tests/TagGroupTest.php @@ -0,0 +1,106 @@ +process($request); + return $request; + } + + /* + * Test with oid and friendly keys for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_Both() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-bare', + "Attributes" => [ + 'urn:oid:2.5.4.31' => ['ADMINS'], + 'member' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['urn:oid:2.5.4.31'] = ['idp|idp-bare|ADMINS']; + $expected["Attributes"]['member'] = ['idp|idp-bare|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } + + + /* + * Test with friendly key for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_Member() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-bare', + "Attributes" => [ + 'member' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['member'] = ['idp|idp-bare|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } + + /* + * Test with oid key for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_Oid() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-bare', + "Attributes" => [ + 'urn:oid:2.5.4.31' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['urn:oid:2.5.4.31'] = ['idp|idp-bare|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } + + /* + * Test with oid key for groups + * @expectedException \SimpleSAML\Error\Exception + */ + public function testTagGroup_IdpGood() + { + $config = [ 'test' => ['value1', 'value2'], ]; + $request = [ + "saml:sp:IdP" => 'idp-good', + "Attributes" => [ + 'urn:oid:2.5.4.31' => ['ADMINS'], + ], + 'metadataPath' => __DIR__ . '/fixtures/metadata/', + ]; + + $expected = $request; + $expected["Attributes"]['urn:oid:2.5.4.31'] = ['idp|idpGood|ADMINS']; + $results = self::processTagGroup($config, $request); + $this->assertEquals($expected, $results); + } +} diff --git a/modules/sildisco/tests/fixtures/metadata/idp-bad-code.php b/modules/sildisco/tests/fixtures/metadata/idp-bad-code.php new file mode 100644 index 00000000..efbce0e7 --- /dev/null +++ b/modules/sildisco/tests/fixtures/metadata/idp-bad-code.php @@ -0,0 +1,12 @@ + [ + 'SingleSignOnService' => 'http://idp-empty/saml2/idp/SSOService.php', + 'IDPNamespace' => '', + ], + 'idp-bad' => [ + 'SingleSignOnService' => 'http://idp-bad/saml2/idp/SSOService.php', + 'IDPNamespace' => 'ba!d!', + ], +]; \ No newline at end of file diff --git a/modules/sildisco/tests/fixtures/metadata/idp-bare.php b/modules/sildisco/tests/fixtures/metadata/idp-bare.php new file mode 100644 index 00000000..c2d28c07 --- /dev/null +++ b/modules/sildisco/tests/fixtures/metadata/idp-bare.php @@ -0,0 +1,7 @@ + [ + 'SingleSignOnService' => 'http://idp-bare/saml2/idp/SSOService.php', + ], +]; diff --git a/modules/sildisco/tests/fixtures/metadata/idp-good.php b/modules/sildisco/tests/fixtures/metadata/idp-good.php new file mode 100644 index 00000000..c06cfa22 --- /dev/null +++ b/modules/sildisco/tests/fixtures/metadata/idp-good.php @@ -0,0 +1,8 @@ + [ + 'SingleSignOnService' => 'http://idp-bare/saml2/idp/SSOService.php', + 'IDPNamespace' => 'idpGood', + ], +]; diff --git a/modules/sildisco/tests/phpunit.xml b/modules/sildisco/tests/phpunit.xml new file mode 100644 index 00000000..807ed707 --- /dev/null +++ b/modules/sildisco/tests/phpunit.xml @@ -0,0 +1,30 @@ + + + + + ../tests/ + + + + + ../tests/ + + ../fixtures/ + + + + + + + + + \ No newline at end of file diff --git a/modules/sildisco/www/betatest.php b/modules/sildisco/www/betatest.php new file mode 100644 index 00000000..b4facf87 --- /dev/null +++ b/modules/sildisco/www/betatest.php @@ -0,0 +1,12 @@ +setData($sessionType, $sessionKey, 1, \SimpleSAML\Session::DATA_TIMEOUT_SESSION_END); + +echo "

Start Beta Testing

"; +echo "

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

"; +echo "

To remove the cookie, just close your browser.

"; diff --git a/modules/sildisco/www/disco.php b/modules/sildisco/www/disco.php new file mode 100644 index 00000000..6c3c08f0 --- /dev/null +++ b/modules/sildisco/www/disco.php @@ -0,0 +1,9 @@ +handleRequest(); diff --git a/modules/sildisco/www/metadata.php b/modules/sildisco/www/metadata.php new file mode 100644 index 00000000..d040bdc3 --- /dev/null +++ b/modules/sildisco/www/metadata.php @@ -0,0 +1,224 @@ +getBoolean('enable.saml20-idp', false)) { + throw new \SimpleSAML\Error\Error('NOACCESS'); +} + +// check if valid local session exists +//if ($config->getBoolean('admin.protectmetadata', false)) { +// Auth::requireAdmin(); +//} + +try { + $idpentityid = isset($_GET['idpentityid']) ? + $_GET['idpentityid'] : + $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted'); + $idpmeta = $metadata->getMetaDataConfig($idpentityid, 'saml20-idp-hosted'); + + $availableCerts = array(); + + $keys = array(); + $certInfo = Crypto::loadPublicKey($idpmeta, false, 'new_'); + if ($certInfo !== null) { + $availableCerts['new_idp.crt'] = $certInfo; + $keys[] = array( + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => true, + 'X509Certificate' => $certInfo['certData'], + ); + $hasNewCert = true; + } else { + $hasNewCert = false; + } + + $certInfo = Crypto::loadPublicKey($idpmeta, true); + $availableCerts['idp.crt'] = $certInfo; + $keys[] = array( + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => ($hasNewCert ? false : true), + 'X509Certificate' => $certInfo['certData'], + ); + + if ($idpmeta->hasValue('https.certificate')) { + $httpsCert = Crypto::loadPublicKey($idpmeta, true, 'https.'); + assert('isset($httpsCert["certData"])'); + $availableCerts['https.crt'] = $httpsCert; + $keys[] = array( + 'type' => 'X509Certificate', + 'signing' => true, + 'encryption' => false, + 'X509Certificate' => $httpsCert['certData'], + ); + } + + $metaArray = array( + 'metadata-set' => 'saml20-idp-remote', + 'entityid' => $idpentityid, + ); + + $ssob = $metadata->getGenerated('SingleSignOnServiceBinding', 'saml20-idp-hosted'); + $slob = $metadata->getGenerated('SingleLogoutServiceBinding', 'saml20-idp-hosted'); + $ssol = $metadata->getGenerated('SingleSignOnService', 'saml20-idp-hosted'); + $slol = $metadata->getGenerated('SingleLogoutService', 'saml20-idp-hosted'); + + if (is_array($ssob)) { + foreach ($ssob as $binding) { + $metaArray['SingleSignOnService'][] = array( + 'Binding' => $binding, + 'Location' => $ssol, + ); + } + } else { + $metaArray['SingleSignOnService'][] = array( + 'Binding' => $ssob, + 'Location' => $ssol, + ); + } + + if (is_array($slob)) { + foreach ($slob as $binding) { + $metaArray['SingleLogoutService'][] = array( + 'Binding' => $binding, + 'Location' => $slol, + ); + } + } else { + $metaArray['SingleLogoutService'][] = array( + 'Binding' => $slob, + 'Location' => $slol, + ); + } + + if (count($keys) === 1) { + $metaArray['certData'] = $keys[0]['X509Certificate']; + } else { + $metaArray['keys'] = $keys; + } + + if ($idpmeta->getBoolean('saml20.sendartifact', false)) { + // Artifact sending enabled + $metaArray['ArtifactResolutionService'][] = array( + 'index' => 0, + 'Location' => HTTP::getBaseURL().'saml2/idp/ArtifactResolutionService.php', + 'Binding' => Constants::BINDING_SOAP, + ); + } + + if ($idpmeta->getBoolean('saml20.hok.assertion', false)) { + // Prepend HoK SSO Service endpoint. + array_unshift($metaArray['SingleSignOnService'], array( + 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => Constants::BINDING_HOK_SSO, + 'Location' => HTTP::getBaseURL().'saml2/idp/SSOService.php' + )); + } + + $metaArray['NameIDFormat'] = $idpmeta->getString( + 'NameIDFormat', + 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + ); + + if ($idpmeta->hasValue('OrganizationName')) { + $metaArray['OrganizationName'] = $idpmeta->getLocalizedString('OrganizationName'); + $metaArray['OrganizationDisplayName'] = $idpmeta->getLocalizedString( + 'OrganizationDisplayName', + $metaArray['OrganizationName'] + ); + + if (!$idpmeta->hasValue('OrganizationURL')) { + throw new \SimpleSAML\Error\Exception('If OrganizationName is set, OrganizationURL must also be set.'); + } + $metaArray['OrganizationURL'] = $idpmeta->getLocalizedString('OrganizationURL'); + } + + if ($idpmeta->hasValue('scope')) { + $metaArray['scope'] = $idpmeta->getArray('scope'); + } + + if ($idpmeta->hasValue('EntityAttributes')) { + $metaArray['EntityAttributes'] = $idpmeta->getArray('EntityAttributes'); + + // check for entity categories + if (Metadata::isHiddenFromDiscovery($metaArray)) { + $metaArray['hide.from.discovery'] = true; + } + } + + if ($idpmeta->hasValue('UIInfo')) { + $metaArray['UIInfo'] = $idpmeta->getArray('UIInfo'); + } + + if ($idpmeta->hasValue('DiscoHints')) { + $metaArray['DiscoHints'] = $idpmeta->getArray('DiscoHints'); + } + + if ($idpmeta->hasValue('RegistrationInfo')) { + $metaArray['RegistrationInfo'] = $idpmeta->getArray('RegistrationInfo'); + } + + if ($idpmeta->hasValue('validate.authnrequest')) { + $metaArray['sign.authnrequest'] = $idpmeta->getBoolean('validate.authnrequest'); + } + + if ($idpmeta->hasValue('redirect.validate')) { + $metaArray['redirect.sign'] = $idpmeta->getBoolean('redirect.validate'); + } + + if ($idpmeta->hasValue('contacts')) { + $contacts = $idpmeta->getArray('contacts'); + foreach ($contacts as $contact) { + $metaArray['contacts'][] = Metadata::getContact($contact); + } + } + + $technicalContactEmail = $config->getString('technicalcontact_email', false); + if ($technicalContactEmail && $technicalContactEmail !== 'na@example.org') { + $techcontact['emailAddress'] = $technicalContactEmail; + $techcontact['name'] = $config->getString('technicalcontact_name', null); + $techcontact['contactType'] = 'technical'; + $metaArray['contacts'][] = Metadata::getContact($techcontact); + } + + $metaBuilder = new \SimpleSAML\Metadata\SAMLBuilder($idpentityid); + $metaBuilder->addMetadataIdP20($metaArray); + $metaBuilder->addOrganizationInfo($metaArray); + + $metaxml = $metaBuilder->getEntityDescriptorText(); + + $metaflat = '$metadata['.var_export($idpentityid, true).'] = '.var_export($metaArray, true).';'; + + // sign the metadata if enabled + $metaxml = \SimpleSAML\Metadata\Signer::sign($metaxml, $idpmeta->toArray(), 'SAML 2 IdP'); + + if (array_key_exists('format', $_GET) && $_GET['format'] == 'xml') { + header('Content-Type: application/xml'); + + echo $metaxml; + exit(0); + } else { + + header('Content-Type: text/html; charset=utf-8'); + + echo '
' . print_r($metaflat, true) . '
'; + exit(0); + } +} catch (Exception $exception) { + throw new \SimpleSAML\Error\Error('METADATA', $exception); +} diff --git a/modules/sildisco/www/sp/discoresp.php b/modules/sildisco/www/sp/discoresp.php new file mode 100644 index 00000000..449309ff --- /dev/null +++ b/modules/sildisco/www/sp/discoresp.php @@ -0,0 +1,34 @@ +startSSO($_REQUEST['idpentityid'], $state); diff --git a/modules/sildisco/www/sp/saml2-acs.php b/modules/sildisco/www/sp/saml2-acs.php new file mode 100644 index 00000000..d5d29aca --- /dev/null +++ b/modules/sildisco/www/sp/saml2-acs.php @@ -0,0 +1,273 @@ +getMetadata(); +try { + $b = \SAML2\Binding::getCurrentBinding(); +} catch (Exception $e) { + // TODO: look for a specific exception + // This is dirty. Instead of checking the message of the exception, \SAML2\Binding::getCurrentBinding() should throw + // a specific exception when the binding is unknown, and we should capture that here + if ($e->getMessage() === 'Unable to find the current binding.') { + throw new \SimpleSAML\Error\Error('ACSPARAMS', $e, 400); + } else { + // do not ignore other exceptions! + throw $e; + } +} + +if ($b instanceof \SAML2\HTTPArtifact) { + $b->setSPMetadata($spMetadata); +} + +$response = $b->receive(); +if (!($response instanceof \SAML2\Response)) { + throw new \SimpleSAML\Error\BadRequest('Invalid message received to AssertionConsumerService endpoint.'); +} + +/** @psalm-var null|string|\SAML2\XML\saml\Issuer $issuer Remove in SSP 2.0 */ +$issuer = $response->getIssuer(); +if ($issuer === null) { + // no Issuer in the response. Look for an unencrypted assertion with an issuer + foreach ($response->getAssertions() as $a) { + if ($a instanceof \SAML2\Assertion) { + // we found an unencrypted assertion, there should be an issuer here + $issuer = $a->getIssuer(); + break; + } + } + /** @psalm-var string|null $issuer Remove in SSP 2.0 */ + if ($issuer === null) { + // no issuer found in the assertions + throw new Exception('Missing in message delivered to AssertionConsumerService.'); + } +} + +if ($issuer instanceof \SAML2\XML\saml\Issuer) { + /** @psalm-var string|null $issuer */ + $issuer = $issuer->getValue(); + if ($issuer === null) { + // no issuer found in the assertions + throw new Exception('Missing in message delivered to AssertionConsumerService.'); + } +} + +$session = \SimpleSAML\Session::getSessionFromRequest(); +$prevAuth = $session->getAuthData($sourceId, 'saml:sp:prevAuth'); +/** @psalm-var string $issuer */ +if ($prevAuth !== null && $prevAuth['id'] === $response->getId() && $prevAuth['issuer'] === $issuer) { + /* OK, it looks like this message has the same issuer + * and ID as the SP session we already have active. We + * therefore assume that the user has somehow triggered + * a resend of the message. + * In that case we may as well just redo the previous redirect + * instead of displaying a confusing error message. + */ + SimpleSAML\Logger::info( + 'Duplicate SAML 2 response detected - ignoring the response and redirecting the user to the correct page.' + ); + if (isset($prevAuth['redirect'])) { + \SimpleSAML\Utils\HTTP::redirectTrustedURL($prevAuth['redirect']); + } + + SimpleSAML\Logger::info('No RelayState or ReturnURL available, cannot redirect.'); + throw new \SimpleSAML\Error\Exception('Duplicate assertion received.'); +} + +$idpMetadata = null; +$state = null; +$stateId = $response->getInResponseTo(); + +if (!empty($stateId)) { + // this should be a response to a request we sent earlier + try { + $state = \SimpleSAML\Auth\State::loadState($stateId, 'saml:sp:sso'); + } catch (Exception $e) { + // something went wrong, + SimpleSAML\Logger::warning('Could not load state specified by InResponseTo: ' . $e->getMessage() . + ' Processing response as unsolicited.'); + } +} + +if ($state) { + // check that the authentication source is correct + assert(array_key_exists('saml:sp:AuthId', $state)); + if ($state['saml:sp:AuthId'] !== $sourceId) { + throw new \SimpleSAML\Error\Exception( + 'The authentication source id in the URL does not match the authentication source which sent the request.' + ); + } + + // check that the issuer is the one we are expecting + assert(array_key_exists('ExpectedIssuer', $state)); + if ($state['ExpectedIssuer'] !== $issuer) { + $idpMetadata = $source->getIdPMetadata($issuer); + $idplist = $idpMetadata->getArrayize('IDPList', []); + if (!in_array($state['ExpectedIssuer'], $idplist, true)) { + SimpleSAML\Logger::warning( + 'The issuer of the response not match to the identity provider we sent the request to.' + ); + } + } +} else { + // this is an unsolicited response + $relaystate = $spMetadata->getString('RelayState', $response->getRelayState()); + $state = [ + 'saml:sp:isUnsolicited' => true, + 'saml:sp:AuthId' => $sourceId, + 'saml:sp:RelayState' => $relaystate === null ? null : \SimpleSAML\Utils\HTTP::checkURLAllowed($relaystate), + ]; +} + +SimpleSAML\Logger::debug('Received SAML2 Response from ' . var_export($issuer, true) . '.'); + +if (is_null($idpMetadata)) { + $idpMetadata = $source->getIdPmetadata($issuer); +} + +try { + $assertions = \SimpleSAML\Module\saml\Message::processResponse($spMetadata, $idpMetadata, $response); +} catch (\SimpleSAML\Module\saml\Error $e) { + // the status of the response wasn't "success" + $e = $e->toException(); + \SimpleSAML\Auth\State::throwException($state, $e); +} + +$authenticatingAuthority = null; +$nameId = null; +$sessionIndex = null; +$expire = null; +$attributes = []; +$foundAuthnStatement = false; + +foreach ($assertions as $assertion) { + // check for duplicate assertion (replay attack) + $store = \SimpleSAML\Store::getInstance(); + if ($store !== false) { + $aID = $assertion->getId(); + if ($store->get('saml.AssertionReceived', $aID) !== null) { + $e = new \SimpleSAML\Error\Exception('Received duplicate assertion.'); + \SimpleSAML\Auth\State::throwException($state, $e); + } + + $notOnOrAfter = $assertion->getNotOnOrAfter(); + if ($notOnOrAfter === null) { + $notOnOrAfter = time() + 24 * 60 * 60; + } else { + $notOnOrAfter += 60; // we allow 60 seconds clock skew, so add it here also + } + + $store->set('saml.AssertionReceived', $aID, true, $notOnOrAfter); + } + + if ($authenticatingAuthority === null) { + $authenticatingAuthority = $assertion->getAuthenticatingAuthority(); + } + if ($nameId === null) { + $nameId = $assertion->getNameId(); + } + if ($sessionIndex === null) { + $sessionIndex = $assertion->getSessionIndex(); + } + if ($expire === null) { + $expire = $assertion->getSessionNotOnOrAfter(); + } + + $attributes = array_merge($attributes, $assertion->getAttributes()); + + if ($assertion->getAuthnInstant() !== null) { + // assertion contains AuthnStatement, since AuthnInstant is a required attribute + $foundAuthnStatement = true; + } +} +$assertion = end($assertions); + +if (!$foundAuthnStatement) { + $e = new \SimpleSAML\Error\Exception('No AuthnStatement found in assertion(s).'); + \SimpleSAML\Auth\State::throwException($state, $e); +} + +if ($expire !== null) { + $logoutExpire = $expire; +} else { + // just expire the logout association 24 hours into the future + $logoutExpire = time() + 24 * 60 * 60; +} + +if (!empty($nameId)) { + // register this session in the logout store + \SimpleSAML\Module\saml\SP\LogoutStore::addSession($sourceId, $nameId, $sessionIndex, $logoutExpire); + + // we need to save the NameID and SessionIndex for logout + $logoutState = [ + 'saml:logout:Type' => 'saml2', + 'saml:logout:IdP' => $issuer, + 'saml:logout:NameID' => $nameId, + 'saml:logout:SessionIndex' => $sessionIndex, + ]; + + $state['saml:sp:NameID'] = $nameId; // no need to mark it as persistent, it already is +} else { + /* + * No NameID provided, we can't logout from this IdP! + * + * Even though interoperability profiles "require" a NameID, the SAML 2.0 standard does not require it to be present + * in assertions. That way, we could have a Subject with only a SubjectConfirmation, or even no Subject element at + * all. + * + * In case we receive a SAML assertion with no NameID, we can be graceful and continue, but we won't be able to + * perform a Single Logout since the SAML logout profile mandates the use of a NameID to identify the individual we + * want to be logged out. In order to minimize the impact of this, we keep logout state information (without saving + * it to the store), marking the IdP as SAML 1.0, which does not implement logout. Then we can safely log the user + * out from the local session, skipping Single Logout upstream to the IdP. + */ + $logoutState = [ + 'saml:logout:Type' => 'saml1', + ]; +} + +$state['LogoutState'] = $logoutState; +$state['saml:AuthenticatingAuthority'] = $authenticatingAuthority; +$state['saml:AuthenticatingAuthority'][] = $issuer; +$state['PersistentAuthData'][] = 'saml:AuthenticatingAuthority'; +$state['saml:AuthnInstant'] = $assertion->getAuthnInstant(); +$state['PersistentAuthData'][] = 'saml:AuthnInstant'; +$state['saml:sp:SessionIndex'] = $sessionIndex; +$state['PersistentAuthData'][] = 'saml:sp:SessionIndex'; +$state['saml:sp:AuthnContext'] = $assertion->getAuthnContextClassRef(); +$state['PersistentAuthData'][] = 'saml:sp:AuthnContext'; + +if ($expire !== null) { + $state['Expire'] = $expire; +} + +// note some information about the authentication, in case we receive the same response again +$state['saml:sp:prevAuth'] = [ + 'id' => $response->getId(), + 'issuer' => $issuer, + 'inResponseTo' => $response->getInResponseTo(), +]; +if (isset($state['\SimpleSAML\Auth\Source.ReturnURL'])) { + $state['saml:sp:prevAuth']['redirect'] = $state['\SimpleSAML\Auth\Source.ReturnURL']; +} elseif (isset($state['saml:sp:RelayState'])) { + $state['saml:sp:prevAuth']['redirect'] = $state['saml:sp:RelayState']; +} +$state['PersistentAuthData'][] = 'saml:sp:prevAuth'; + +$source->handleResponse($state, $issuer, $attributes); +assert(false); \ No newline at end of file diff --git a/modules/sildisco/www/sp/saml2-logout.php b/modules/sildisco/www/sp/saml2-logout.php new file mode 100644 index 00000000..05d9c14b --- /dev/null +++ b/modules/sildisco/www/sp/saml2-logout.php @@ -0,0 +1,155 @@ +getMessage() === 'Unable to find the current binding.') { + throw new \SimpleSAML\Error\Error('SLOSERVICEPARAMS', $e, 400); + } else { + throw $e; // do not ignore other exceptions! + } +} +$message = $binding->receive(); + +$issuer = $message->getIssuer(); +if ($issuer instanceof \SAML2\XML\saml\Issuer) { + $idpEntityId = $issuer->getValue(); +} else { + $idpEntityId = $issuer; +} + +if ($idpEntityId === null) { + // Without an issuer we have no way to respond to the message. + throw new \SimpleSAML\Error\BadRequest('Received message on logout endpoint without issuer.'); +} + +/** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */ +$spEntityId = $source->getEntityId(); + +$metadata = \SimpleSAML\Metadata\MetaDataStorageHandler::getMetadataHandler(); +$idpMetadata = $source->getIdPMetadata($idpEntityId); +$spMetadata = $source->getMetadata(); + +\SimpleSAML\Module\saml\Message::validateMessage($idpMetadata, $spMetadata, $message); + +$destination = $message->getDestination(); +if ($destination !== null && $destination !== \SimpleSAML\Utils\HTTP::getSelfURLNoQuery()) { + throw new \SimpleSAML\Error\Exception('Destination in logout message is wrong.'); +} + +if ($message instanceof \SAML2\LogoutResponse) { + $relayState = $message->getRelayState(); + if ($relayState === null) { + // Somehow, our RelayState has been lost. + throw new \SimpleSAML\Error\BadRequest('Missing RelayState in logout response.'); + } + + if (!$message->isSuccess()) { + \SimpleSAML\Logger::warning( + 'Unsuccessful logout. Status was: ' . \SimpleSAML\Module\saml\Message::getResponseError($message) + ); + } + + $state = \SimpleSAML\Auth\State::loadState($relayState, 'saml:slosent'); + $state['saml:sp:LogoutStatus'] = $message->getStatus(); + \SimpleSAML\Auth\Source::completeLogout($state); +} elseif ($message instanceof \SAML2\LogoutRequest) { + \SimpleSAML\Logger::debug('module/sildisco/sp/logout: Request from ' . $idpEntityId); // GTIS + \SimpleSAML\Logger::stats('saml20-idp-SLO idpinit ' . $spEntityId . ' ' . $idpEntityId); + + if ($message->isNameIdEncrypted()) { + try { + $keys = \SimpleSAML\Module\saml\Message::getDecryptionKeys($idpMetadata, $spMetadata); + } catch (\Exception $e) { + throw new \SimpleSAML\Error\Exception('Error decrypting NameID: ' . $e->getMessage()); + } + + $blacklist = \SimpleSAML\Module\saml\Message::getBlacklistedAlgorithms($idpMetadata, $spMetadata); + + $lastException = null; + foreach ($keys as $i => $key) { + try { + $message->decryptNameId($key, $blacklist); + \SimpleSAML\Logger::debug('Decryption with key #' . $i . ' succeeded.'); + $lastException = null; + break; + } catch (\Exception $e) { + \SimpleSAML\Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage()); + $lastException = $e; + } + } + if ($lastException !== null) { + throw $lastException; + } + } + + $nameId = $message->getNameId(); + $sessionIndexes = $message->getSessionIndexes(); + + /** @psalm-suppress PossiblyNullArgument This will be fixed in saml2 5.0 */ + $numLoggedOut = \SimpleSAML\Module\saml\SP\LogoutStore::logoutSessions($sourceId, $nameId, $sessionIndexes); + if ($numLoggedOut === false) { + // This type of logout was unsupported. Use the old method + $source->handleLogout($idpEntityId); + $numLoggedOut = count($sessionIndexes); + } + + // Create and send response + $lr = \SimpleSAML\Module\saml\Message::buildLogoutResponse($spMetadata, $idpMetadata); + $lr->setRelayState($message->getRelayState()); + $lr->setInResponseTo($message->getId()); + + if ($numLoggedOut < count($sessionIndexes)) { + \SimpleSAML\Logger::warning('Logged out of ' . $numLoggedOut . ' of ' . count($sessionIndexes) . ' sessions.'); + } + + /** @var array $dst */ + $dst = $idpMetadata->getEndpointPrioritizedByBinding( + 'SingleLogoutService', + [ + \SAML2\Constants::BINDING_HTTP_REDIRECT, + \SAML2\Constants::BINDING_HTTP_POST + ] + ); + + if (!($binding instanceof \SAML2\SOAP)) { + $binding = \SAML2\Binding::getBinding($dst['Binding']); + if (isset($dst['ResponseLocation'])) { + $dst = $dst['ResponseLocation']; + } else { + $dst = $dst['Location']; + } + $binding->setDestination($dst); + } + $lr->setDestination($dst); + + $binding->send($lr); +} else { + throw new \SimpleSAML\Error\BadRequest('Unknown message received on logout endpoint: ' . get_class($message)); +} From b33c8953a6b2c261c599f480e89a3962e1cc7efd Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 20 May 2024 16:30:55 +0800 Subject: [PATCH 57/92] fixed the sildisco integration tests for running in ssp-base --- actions-services.yml | 33 ++--- behat.yml | 10 ++ development/{idp-local => }/UserPass.php | 0 development/enable-exampleauth-module.sh | 7 - development/hub/metadata/idp-remote.php | 63 ++++++++- development/hub/metadata/sp-remote.php | 44 ++++-- development/idp-local/config/authsources.php | 24 ++++ development/idp-local/config/config.php | 1 + development/idp2-local/config/authsources.php | 26 ++++ development/idp2-local/config/config.php | 1 + .../idp2-local/metadata/saml20-idp-hosted.php | 2 +- development/sp2-local/config/authsources.php | 3 + development/sp3-local/config/authsources.php | 24 ++-- development/sp3-local/config/config.php | 91 ++++++++---- .../sp3-local/metadata/saml20-idp-remote.php | 9 -- docker-compose.yml | 30 ++-- features/Sp1Idp1Sp2Idp2Sp3.feature | 15 +- features/Sp1Idp2Sp2Sp3Idp1.feature | 15 +- features/Sp2Idp2Sp1Idp1Sp3.feature | 16 +-- features/Sp2Idp2Sp1Idp2Sp3.feature | 16 +-- features/Sp3Idp1Sp1Idp1Sp2Idp2.feature | 16 +-- features/WwwMetadataCept.feature | 10 +- features/ZSp1Idp1BetaSp1Idp3.feature | 7 +- features/bootstrap/FeatureContext.php | 129 +++++++++++++++++- features/bootstrap/SilDiscoContext.php | 69 +++++++++- 25 files changed, 499 insertions(+), 162 deletions(-) rename development/{idp-local => }/UserPass.php (100%) delete mode 100755 development/enable-exampleauth-module.sh diff --git a/actions-services.yml b/actions-services.yml index 524bebf5..9c3cef6d 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -16,7 +16,10 @@ services: - ssp-hub.local - ssp-idp1.local - ssp-idp2.local + - ssp-idp3.local - ssp-sp1.local + - ssp-sp2.local + - ssp-sp3.local - pwmanager.local - test-browser environment: @@ -87,11 +90,8 @@ services: - ./development/idp-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php - # Misc. files needed - - ./development/enable-exampleauth-module.sh:/data/enable-exampleauth-module.sh - # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code - - ./development/idp-local/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -100,7 +100,6 @@ services: - ./features:/data/features command: > bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && - /data/enable-exampleauth-module.sh && /data/run.sh" environment: ADMIN_EMAIL: "john_doe@there.com" @@ -139,11 +138,9 @@ services: - ./development/idp2-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp2-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php - # Local modules - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ports: - - "8086:80" + # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + environment: ADMIN_EMAIL: "john_doe@there.com" ADMIN_PASS: "b" @@ -153,8 +150,8 @@ services: SHOW_SAML_ERRORS: "true" THEME_USE: "material:material" - idp3: - image: silintl/ssp-base:develop + ssp-idp3.local: + build: . volumes: # Utilize custom certs - ./development/idp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert @@ -199,8 +196,8 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" - sp2: - image: silintl/ssp-base:develop + ssp-sp2.local: + build: . volumes: # Utilize custom certs - ./development/sp2-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert @@ -221,8 +218,8 @@ services: - SAML20_IDP_ENABLE=false - ADMIN_PROTECT_INDEX_PAGE=false - sp3: - image: silintl/ssp-base:develop + ssp-sp3.local: + build: . volumes: # Utilize custom certs - ./development/sp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert @@ -269,8 +266,6 @@ services: # the broker and brokerDb containers are used by the silauth module broker: image: silintl/idp-id-broker:latest - ports: - - "80" depends_on: - brokerDb environment: @@ -300,8 +295,6 @@ services: brokerDb: image: mariadb:10 - ports: - - "3306" environment: MYSQL_ROOT_PASSWORD: "r00tp@ss!" MYSQL_DATABASE: "broker" diff --git a/behat.yml b/behat.yml index e05a2b46..6306126b 100644 --- a/behat.yml +++ b/behat.yml @@ -18,6 +18,16 @@ default: profilereview_features: paths: [ '%paths.base%//features//profilereview.feature' ] contexts: [ 'ProfileReviewContext' ] + sildisco_features: + contexts: ['SilDiscoContext'] + paths: + - '%paths.base%//features//Sp1Idp1Sp2Idp2Sp3.feature' + - '%paths.base%//features//Sp1Idp2Sp2Sp3Idp1.feature' + - '%paths.base%//features//Sp2Idp2Sp1Idp1Sp3.feature' + - '%paths.base%//features//Sp2Idp2Sp1Idp2Sp3.feature' + - '%paths.base%//features//Sp3Idp1Sp1Idp1Sp2Idp2.feature' + - '%paths.base%//features//WwwMetadataCept.feature' + - '%paths.base%//features//ZSp1Idp1BetaSp1Idp3.feature' status_features: paths: [ '%paths.base%//features//status.feature' ] contexts: [ 'StatusContext' ] diff --git a/development/idp-local/UserPass.php b/development/UserPass.php similarity index 100% rename from development/idp-local/UserPass.php rename to development/UserPass.php diff --git a/development/enable-exampleauth-module.sh b/development/enable-exampleauth-module.sh deleted file mode 100755 index 5e60e1f7..00000000 --- a/development/enable-exampleauth-module.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -mkdir -p /data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth -touch /data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/enable diff --git a/development/hub/metadata/idp-remote.php b/development/hub/metadata/idp-remote.php index 2b324bf4..a9ba60fa 100644 --- a/development/hub/metadata/idp-remote.php +++ b/development/hub/metadata/idp-remote.php @@ -8,13 +8,13 @@ */ return [ /* - * Guest IdP. Sign in with an "a" (lower case) as the password + * IdP 1 */ 'http://ssp-idp1.local:8085' => [ 'metadata-set' => 'saml20-idp-remote', 'entityid' => 'http://ssp-idp1.local:8085', 'name' => [ - 'en' => 'IDP 1', + 'en' => 'IDP 1:8085', ], 'IDPNamespace' => 'IDP-1-custom-port', 'logoCaption' => 'IDP-1:8085 staff', @@ -26,6 +26,10 @@ 'SingleSignOnService' => 'http://ssp-idp1.local:8085/saml2/idp/SSOService.php', 'SingleLogoutService' => 'http://ssp-idp1.local:8085/saml2/idp/SingleLogoutService.php', 'certData' => 'MIIDzzCCAregAwIBAgIJAPlZYTAQSIbHMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTQ1WhcNMjYxMDE3MTIzMTQ1WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArssOaeKbdOQFpN6bBolwSJ/6QFBXA73Sotg60anx9v6aYdUTmi+b7SVtvOmHDgsD5X8pN/6Z11QCZfTYg2nW3ZevGZsj8W/R6C8lRLHzWUr7e7DXKfj8GKZptHlUs68kn0ndNVt9r/+irJe9KBdZ+4kAihykomNdeZg06bvkklxVcvpkOfLTQzEqJAmISPPIeOXes6hXORdqLuRNTuIKarcZ9rstLnpgAs2TE4XDOrSuUg3XFnM05eDpFQpUb0RXWcD16mLCPWw+CPrGoCfoftD5ZGfll+W2wZ7d0kQ4TbCpNyxQH35q65RPVyVNPgSNSsFFkmdcqP9DsFqjJ8YC6wIDAQABo1AwTjAdBgNVHQ4EFgQUD6oyJKOPPhvLQpDCC3027QcuQwUwHwYDVR0jBBgwFoAUD6oyJKOPPhvLQpDCC3027QcuQwUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAA6tCLHJQGfXGdFerQ3J0wUu8YDSLb0WJqPtGdIuyeiywR5ooJf8G/jjYMPgZArepLQSSi6t8/cjEdkYWejGnjMG323drQ9M1sKMUhOJF4po9R3t7IyvGAL3fSqjXA8JXH5MuGuGtChWxaqhduA0dBJhFAtAXQ61IuIQF7vSFxhTwCvJnaWdWD49sG5OqjCfgIQdY/mw70e45rLnR/bpfoigL67sTJxy+Kx2ogbvMR6lITByOEQFMt7BYpMtXrwvKUM7k9NOo1jREmJacC8PTx//jRhCWwzUj1RsfIri24BuITrawwqMsYl8DZiiwMpjUf9m4NPaf4E7+QRpzo+MCcg==', + + // NOTE: This breaks being able to test the hub's authentication sources + // since the hub doesn't create an SP entry in the session + 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local', 'http://ssp-sp3.local'], ], 'http://ssp-idp1.local' => [ 'metadata-set' => 'saml20-idp-remote', @@ -44,16 +48,20 @@ 'SingleLogoutService' => 'http://ssp-idp1.local/saml2/idp/SingleLogoutService.php', // 'certFingerprint' => 'c9ed4dfb07caf13fc21e0fec1572047eb8a7a4cb' 'certData' => 'MIIDzzCCAregAwIBAgIJAPlZYTAQSIbHMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTQ1WhcNMjYxMDE3MTIzMTQ1WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArssOaeKbdOQFpN6bBolwSJ/6QFBXA73Sotg60anx9v6aYdUTmi+b7SVtvOmHDgsD5X8pN/6Z11QCZfTYg2nW3ZevGZsj8W/R6C8lRLHzWUr7e7DXKfj8GKZptHlUs68kn0ndNVt9r/+irJe9KBdZ+4kAihykomNdeZg06bvkklxVcvpkOfLTQzEqJAmISPPIeOXes6hXORdqLuRNTuIKarcZ9rstLnpgAs2TE4XDOrSuUg3XFnM05eDpFQpUb0RXWcD16mLCPWw+CPrGoCfoftD5ZGfll+W2wZ7d0kQ4TbCpNyxQH35q65RPVyVNPgSNSsFFkmdcqP9DsFqjJ8YC6wIDAQABo1AwTjAdBgNVHQ4EFgQUD6oyJKOPPhvLQpDCC3027QcuQwUwHwYDVR0jBBgwFoAUD6oyJKOPPhvLQpDCC3027QcuQwUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAA6tCLHJQGfXGdFerQ3J0wUu8YDSLb0WJqPtGdIuyeiywR5ooJf8G/jjYMPgZArepLQSSi6t8/cjEdkYWejGnjMG323drQ9M1sKMUhOJF4po9R3t7IyvGAL3fSqjXA8JXH5MuGuGtChWxaqhduA0dBJhFAtAXQ61IuIQF7vSFxhTwCvJnaWdWD49sG5OqjCfgIQdY/mw70e45rLnR/bpfoigL67sTJxy+Kx2ogbvMR6lITByOEQFMt7BYpMtXrwvKUM7k9NOo1jREmJacC8PTx//jRhCWwzUj1RsfIri24BuITrawwqMsYl8DZiiwMpjUf9m4NPaf4E7+QRpzo+MCcg==', + + // NOTE: This breaks being able to test the hub's authentication sources + // since the hub doesn't create an SP entry in the session + 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local', 'http://ssp-sp3.local'], ], /* - * IdP2. Sign in with a "b" (lower case) as the password + * IdP 2 */ 'http://ssp-idp2.local:8086' => [ 'metadata-set' => 'saml20-idp-remote', 'entityid' => 'http://ssp-idp2.local:8086', 'name' => [ - 'en' => 'IDP 2', + 'en' => 'IDP 2:8086', ], 'IDPNamespace' => 'IDP-2-custom-port', 'logoCaption' => 'IDP-2:8086 staff', @@ -66,6 +74,9 @@ 'SingleSignOnService' => 'http://ssp-idp2.local:8086/saml2/idp/SSOService.php', 'SingleLogoutService' => 'http://ssp-idp2.local:8086/saml2/idp/SingleLogoutService.php', 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', + + // limit which Sps can use this IdP + 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local'], ], 'http://ssp-idp2.local' => [ 'metadata-set' => 'saml20-idp-remote', @@ -84,5 +95,49 @@ 'SingleSignOnService' => 'http://ssp-idp2.local/saml2/idp/SSOService.php', 'SingleLogoutService' => 'http://ssp-idp2.local/saml2/idp/SingleLogoutService.php', 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', + + // limit which Sps can use this IdP + 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local'], + ], + + /* + * IdP 3 + */ + 'http://ssp-idp3.local:8087' => [ + 'metadata-set' => 'saml20-idp-remote', + 'entityid' => 'http://ssp-idp3.local:8087', + 'name' => [ + 'en' => 'IDP 3:8087', + ], + 'IDPNamespace' => 'IDP-3-custom-port', + 'logoCaption' => 'IDP-3:8087 staff', + 'enabled' => false, + 'betaEnabled' => true, + 'logoURL' => 'https://dummyimage.com/125x125/0f4fbd/ffffff.png&text=IDP+3+8087', + + 'description' => 'Local IDP3 for testing SSP Hub (custom port)', + + 'SingleSignOnService' => 'http://ssp-idp3.local:8087/saml2/idp/SSOService.php', + 'SingleLogoutService' => 'http://ssp-idp3.local:8087/saml2/idp/SingleLogoutService.php', + 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', ], + 'http://ssp-idp3.local' => [ + 'metadata-set' => 'saml20-idp-remote', + 'entityid' => 'http://ssp-idp3.local', + 'name' => [ + 'en' => 'IDP 3', + ], + 'IDPNamespace' => 'IDP-3', + 'logoCaption' => 'IDP-3 staff', + 'enabled' => false, + 'betaEnabled' => true, + 'logoURL' => 'https://dummyimage.com/125x125/0f4fbd/ffffff.png&text=IDP+3', + + 'description' => 'Local IDP3 for testing SSP Hub', + + 'SingleSignOnService' => 'http://ssp-idp3.local/saml2/idp/SSOService.php', + 'SingleLogoutService' => 'http://ssp-idp3.local/saml2/idp/SingleLogoutService.php', + 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', + ], + ]; diff --git a/development/hub/metadata/sp-remote.php b/development/hub/metadata/sp-remote.php index 988ebec7..a45271e7 100644 --- a/development/hub/metadata/sp-remote.php +++ b/development/hub/metadata/sp-remote.php @@ -10,11 +10,7 @@ * Example SimpleSAMLphp SAML 2.0 SP */ 'http://ssp-sp1.local:8081' => [ - 'IDPList' => [ - 'http://ssp-idp1.local:8085', - 'http://ssp-idp2.local:8086', - ], - 'name' => "SP Local", + 'name' => "SP1 (custom port)", 'AssertionConsumerService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-acs.php/ssp-hub-custom-port', 'SingleLogoutService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-logout.php/ssp-hub-custom-port', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', @@ -22,11 +18,7 @@ ], 'http://ssp-sp1.local' => [ - 'IDPList' => [ - 'http://ssp-idp1.local', - 'http://ssp-idp2.local', - ], - 'name' => "SP Local", + 'name' => "SP1", 'AssertionConsumerService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-acs.php/ssp-hub', 'SingleLogoutService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-logout.php/ssp-hub', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', @@ -39,7 +31,7 @@ 'IDPList' => [ 'http://ssp-idp2.local:8086', ], - 'name' => 'SP 2 (custom port)', + 'name' => 'SP2 (custom port)', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'assertion.encryption' => true, ], @@ -50,7 +42,35 @@ 'IDPList' => [ 'http://ssp-idp2.local', ], - 'name' => 'SP 2', + 'name' => 'SP2', + 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', + 'assertion.encryption' => true, + ], + + // for test purposes, SP3 should be on the SPList entry of idp2 + + 'http://ssp-sp3.local:8083' => [ + 'AssertionConsumerService' => 'http://ssp-sp3.local:8083/module.php/saml/sp/saml2-acs.php/ssp-hub', + 'SingleLogoutService' => 'http://ssp-sp3.local:8083/module.php/saml/sp/saml2-logout.php/ssp-hub', + 'IDPList' => [ + 'http://ssp-idp1.local:8085', + 'http://ssp-idp2.local:8086', // overruled by Idp2 + 'http://ssp-idp3.local:8087' + ], + 'name' => 'SP3 (custom port)', + 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', + 'assertion.encryption' => true, + ], + + 'http://ssp-sp3.local' => [ + 'AssertionConsumerService' => 'http://ssp-sp3.local/module.php/saml/sp/saml2-acs.php/ssp-hub', + 'SingleLogoutService' => 'http://ssp-sp3.local/module.php/saml/sp/saml2-logout.php/ssp-hub', + 'IDPList' => [ + 'http://ssp-idp1.local', + 'http://ssp-idp2.local', // overruled by Idp2 + 'http://ssp-idp3.local' + ], + 'name' => 'SP3', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', 'assertion.encryption' => true, ], diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index 28c8124a..b03e4fcb 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -1186,5 +1186,29 @@ ], 'manager_email' => ['manager@example.com'], ], + + // sildisco test user + 'sildisco_idp1:sildisco_password' => [ + 'eduPersonPrincipalName' => ['sildisco@idp1'], + 'eduPersonTargetID' => ['57de1930-c5d2-4f6f-9318-d85a939c45d8'], + 'sn' => ['IDP1'], + 'givenName' => ['SilDisco'], + 'mail' => ['sildisco_idp1@example.com'], + 'employeeNumber' => ['50001'], + 'cn' => ['SILDISCO_IDP1'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'no', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + ], + 'profile_review' => 'no' + ], + ], ]; diff --git a/development/idp-local/config/config.php b/development/idp-local/config/config.php index 685f66f1..db650c58 100644 --- a/development/idp-local/config/config.php +++ b/development/idp-local/config/config.php @@ -599,6 +599,7 @@ 'profilereview' => true, 'silauth' => true, 'sildisco' => true, + 'exampleauth' => true, ], diff --git a/development/idp2-local/config/authsources.php b/development/idp2-local/config/authsources.php index a5d7d017..197f61b1 100644 --- a/development/idp2-local/config/authsources.php +++ b/development/idp2-local/config/authsources.php @@ -10,4 +10,30 @@ 'core:AdminPassword', ], + 'example-userpass' => [ + 'exampleauth:UserPass', + + // sildisco test user + 'sildisco_idp2:sildisco_password' => [ + 'eduPersonPrincipalName' => ['sildisco@idp2'], + 'eduPersonTargetID' => ['57de2930-c5d2-4f6f-9328-d85a939c45d8'], + 'sn' => ['IDP2'], + 'givenName' => ['SilDisco'], + 'mail' => ['sildisco_idp2@example.com'], + 'employeeNumber' => ['50002'], + 'cn' => ['SILDISCO_IDP2'], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+6 months')), + ], + 'mfa' => [ + 'prompt' => 'no', + 'add' => 'no', + 'options' => [], + ], + 'method' => [ + 'add' => 'no', + ], + 'profile_review' => 'no' + ], + ] ]; diff --git a/development/idp2-local/config/config.php b/development/idp2-local/config/config.php index 685f66f1..db650c58 100644 --- a/development/idp2-local/config/config.php +++ b/development/idp2-local/config/config.php @@ -599,6 +599,7 @@ 'profilereview' => true, 'silauth' => true, 'sildisco' => true, + 'exampleauth' => true, ], diff --git a/development/idp2-local/metadata/saml20-idp-hosted.php b/development/idp2-local/metadata/saml20-idp-hosted.php index 78ff4405..ad7b8705 100644 --- a/development/idp2-local/metadata/saml20-idp-hosted.php +++ b/development/idp2-local/metadata/saml20-idp-hosted.php @@ -21,7 +21,7 @@ * Authentication source to use. Must be one that is configured in * 'config/authsources.php'. */ - 'auth' => 'admin', + 'auth' => 'example-userpass', ]; // Copy configuration for port 80 and modify host. diff --git a/development/sp2-local/config/authsources.php b/development/sp2-local/config/authsources.php index b48c271c..60f76215 100644 --- a/development/sp2-local/config/authsources.php +++ b/development/sp2-local/config/authsources.php @@ -27,6 +27,9 @@ // The URL to the discovery service. // Can be NULL/unset, in which case a builtin discovery service will be used. 'discoURL' => null, + + // Specify what private key to use (such as for decrypting assertions). + 'privatekey' => 'ssp-hub-sp2.pem', ], 'ssp-hub-custom-port' => [ diff --git a/development/sp3-local/config/authsources.php b/development/sp3-local/config/authsources.php index aaf9e770..ebc96e6c 100644 --- a/development/sp3-local/config/authsources.php +++ b/development/sp3-local/config/authsources.php @@ -1,19 +1,19 @@ array( + 'admin' => [ // The default is to use core:AdminPassword, but it can be replaced with // any authentication source. 'core:AdminPassword', - ), + ], // An authentication source which can authenticate against both SAML 2.0 // and Shibboleth 1.3 IdPs. - 'ssp-hub' => array( + 'ssp-hub' => [ 'saml:SP', // The entity ID of this SP. @@ -27,9 +27,12 @@ // The URL to the discovery service. // Can be NULL/unset, in which case a builtin discovery service will be used. 'discoURL' => null, - ), - 'hub4tests' => array( + // Specify what private key to use (such as for decrypting assertions). + 'privatekey' => 'ssp-hub-sp3.pem', + ], + + 'ssp-hub-custom-port' => [ 'saml:SP', // The entity ID of this SP. @@ -38,12 +41,13 @@ // The entity ID of the IdP this should SP should contact. // Can be NULL/unset, in which case the user will be shown a list of available IdPs. - 'idp' => 'hub4tests', + 'idp' => 'ssp-hub.local', // The URL to the discovery service. // Can be NULL/unset, in which case a builtin discovery service will be used. 'discoURL' => null, - ), - -); + // Specify what private key to use (such as for decrypting assertions). + 'privatekey' => 'ssp-hub-sp3.pem', + ], +]; diff --git a/development/sp3-local/config/config.php b/development/sp3-local/config/config.php index c773de78..1686e5a2 100644 --- a/development/sp3-local/config/config.php +++ b/development/sp3-local/config/config.php @@ -5,6 +5,14 @@ */ use Sil\PhpEnv\Env; +$logLevels = [ + 'ERR' => SimpleSAML\Logger::ERR, // No statistics, only errors + 'WARNING' => SimpleSAML\Logger::WARNING, // No statistics, only warnings/errors + 'NOTICE' => SimpleSAML\Logger::NOTICE, // Statistics and errors + 'INFO' => SimpleSAML\Logger::INFO, // Verbose logs + 'DEBUG' => SimpleSAML\Logger::DEBUG, // Full debug logs - not recommended for production +]; + /* * Get config settings from ENV vars or set defaults */ @@ -20,7 +28,9 @@ $ADMIN_PROTECT_INDEX_PAGE = Env::get('ADMIN_PROTECT_INDEX_PAGE', true); $SHOW_SAML_ERRORS = Env::get('SHOW_SAML_ERRORS', false); $TIMEZONE = Env::get('TIMEZONE', 'GMT'); -$LOGGING_HANDLER = Env::get('LOGGING_HANDLER', 'syslog'); +$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 @@ -28,10 +38,8 @@ $SESSION_REMEMBERME_LIFETIME = (int)(Env::get('SESSION_REMEMBERME_LIFETIME', (14 * 86400))); // 14 days $SECURE_COOKIE = Env::get('SECURE_COOKIE', true); $THEME_USE = Env::get('THEME_USE', 'default'); -$MEMCACHE_STORE_EXPIRES = (int)(Env::get('MEMCACHE_STORE_EXPIRES', (36 * 60 * 60))); // 36 hours. $SAML20_IDP_ENABLE = Env::get('SAML20_IDP_ENABLE', true); $GOOGLE_ENABLE = Env::get('GOOGLE_ENABLE', false); -$FORCE_DISCOVERY = Env::get('FORCE_DISCOVERY', false); $config = [ @@ -64,15 +72,42 @@ /* - * If you enable this option, simpleSAMLphp will log all sent and received messages - * to the log file. + * The 'debug' option allows you to control how SimpleSAMLphp behaves in certain + * situations where further action may be taken + * + * It can be left unset, in which case, debugging is switched off for all actions. + * If set, it MUST be an array containing the actions that you want to enable, or + * alternatively a hashed array where the keys are the actions and their + * corresponding values are booleans enabling or disabling each particular action. + * + * SimpleSAMLphp provides some pre-defined actions, though modules could add new + * actions here. Refer to the documentation of every module to learn if they + * allow you to set any more debugging actions. * - * This option also enables logging of the messages that are encrypted and decrypted. + * The pre-defined actions are: * - * Note: The messages are logged with the DEBUG log level, so you also need to set - * the 'logging.level' option to LOG_DEBUG. + * - 'saml': this action controls the logging of SAML messages exchanged with other + * entities. When enabled ('saml' is present in this option, or set to true), all + * SAML messages will be logged, including plaintext versions of encrypted + * messages. + * + * - 'backtraces': this action controls the logging of error backtraces. If you + * want to log backtraces so that you can debug any possible errors happening in + * SimpleSAMLphp, enable this action (add it to the array or set it to true). + * + * - 'validatexml': this action allows you to validate SAML documents against all + * the relevant XML schemas. SAML 1.1 messages or SAML metadata parsed with + * the XML to SimpleSAMLphp metadata converter or the metaedit module will + * validate the SAML documents if this option is enabled. + * + * If you want to disable debugging completely, unset this option or set it to an + * empty array. */ - 'debug' => false, + 'debug' => [ + 'saml' => $ENABLE_DEBUG, + 'backtraces' => true, + 'validatexml' => $ENABLE_DEBUG, + ], /* * When showerrors is enabled, all error messages and stack traces will be output @@ -85,11 +120,11 @@ '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('\SimpleSAML\Module\example\Error\Show', 'show'), + * 'errors.show_function' => array('sspmod_example_Error_Show', 'show'), */ /* @@ -139,18 +174,18 @@ * 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 + * SimpleSAML\Logger::ERR No statistics, only errors + * SimpleSAML\Logger::WARNING No statistics, only warnings/errors + * SimpleSAML\Logger::NOTICE Statistics and errors + * SimpleSAML\Logger::INFO Verbose logs + * SimpleSAML\Logger::DEBUG Full debug logs - not reccomended for production * * Choose logging handler. * - * Options: [syslog,file,errorlog] + * Options: [syslog,file,errorlog,stderr] * */ - 'logging.level' => \SimpleSAML\Logger::NOTICE, + 'logging.level' => $logLevels[$LOGGING_LEVEL], 'logging.handler' => $LOGGING_HANDLER, /* @@ -249,9 +284,9 @@ * * 'module.enable' => array( * // Setting to TRUE enables. - * 'exampleauth' => true, + * 'exampleauth' => TRUE, * // Setting to FALSE disables. - * 'saml' => false, + * 'saml' => FALSE, * // Unset or NULL uses default. * 'core' => NULL, * ), @@ -261,6 +296,12 @@ 'module.enable' => [ // Setting to TRUE enables. 'authgoogle' => $GOOGLE_ENABLE, + 'expirychecker' => true, + 'material' => true, + 'mfa' => true, + 'profilereview' => true, + 'silauth' => true, + 'sildisco' => true, ], /* @@ -378,7 +419,7 @@ * See docs/simplesamlphp-advancedfeatures.txt for function code example. * * Example: - * 'session.check_function' => array('\SimpleSAML\Module\example\Util', 'checkSession'), + * 'session.check_function' => array('sspmod_example_Util', 'checkSession'), */ /* @@ -405,15 +446,15 @@ 'language.cookie.lifetime' => (60 * 60 * 24 * 900), /** - * Custom getLanguage function called from \SimpleSAML\XHTML\Template::getLanguage(). + * Custom getLanguage function called from SimpleSAML_XHTML_Template::getLanguage(). * Function should return language code of one of the available languages or NULL. - * See \SimpleSAML\XHTML\Template::getLanguage() source code for more info. + * See SimpleSAML_XHTML_Template::getLanguage() source code for more info. * * This option can be used to implement a custom function for determining * the default language for the user. * * Example: - * 'language.get_language_function' => array('\SimpleSAML\Module\example\Template', 'getLanguage'), + * 'language.get_language_function' => array('sspmod_example_Template', 'getLanguage'), */ /* @@ -546,7 +587,7 @@ 'class' => 'consent:Consent', 'store' => 'consent:Cookie', 'focus' => 'yes', - 'checked' => true + 'checked' => TRUE ), */ diff --git a/development/sp3-local/metadata/saml20-idp-remote.php b/development/sp3-local/metadata/saml20-idp-remote.php index fb1c762f..f518f827 100644 --- a/development/sp3-local/metadata/saml20-idp-remote.php +++ b/development/sp3-local/metadata/saml20-idp-remote.php @@ -11,18 +11,9 @@ * Guest IdP. allows users to sign up and register. Great for testing! */ $metadata['ssp-hub.local'] = [ - // 'SingleSignOnService' => 'http://ssp-hub.local/saml2/idp/SSOService.php', 'SingleSignOnService' => 'http://ssp-hub.local/saml2/idp/SSOService.php', 'SingleLogoutService' => 'http://ssp-hub.local/saml2/idp/SingleLogoutService.php', 'certData' =>'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', ]; -/* - * For automated testing - */ -$metadata['hub4tests'] = [ - 'SingleSignOnService' => 'http://hub4tests/saml2/idp/SSOService.php', - 'SingleLogoutService' => 'http://hub4tests/saml2/idp/SingleLogoutService.php', - 'certData' =>'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', -]; diff --git a/docker-compose.yml b/docker-compose.yml index 652dba21..90b6140a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,10 @@ services: - ssp-hub.local - ssp-idp1.local - ssp-idp2.local + - ssp-idp3.local - ssp-sp1.local + - ssp-sp2.local + - ssp-sp3.local - pwmanager.local - test-browser environment: @@ -158,11 +161,8 @@ services: - ./development/idp-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php - # Misc. files needed - - ./development/enable-exampleauth-module.sh:/data/enable-exampleauth-module.sh - # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code - - ./development/idp-local/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php # Enable checking our test metadata - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -177,7 +177,6 @@ services: - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth command: > bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && - /data/enable-exampleauth-module.sh && /data/run.sh" ports: - "8085:80" @@ -219,6 +218,9 @@ services: - ./development/idp2-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp2-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + # Customized SSP code -- TODO: make a better solution that doesn't require hacking SSP code + - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php + # Local modules - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker @@ -235,8 +237,8 @@ services: SHOW_SAML_ERRORS: "true" THEME_USE: "material:material" - idp3: - image: silintl/ssp-base:develop + ssp-idp3.local: + build: . volumes: # Utilize custom certs - ./development/idp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert @@ -248,6 +250,12 @@ services: # Utilize custom metadata - ./development/idp3-local/metadata/saml20-idp-hosted.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-hosted.php - ./development/idp3-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php + + # Local modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth ports: - "8087:80" env_file: @@ -292,7 +300,7 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" - sp2: + ssp-sp2.local: build: . volumes: # Utilize custom certs @@ -321,8 +329,8 @@ services: SAML20_IDP_ENABLE: "false" ADMIN_PROTECT_INDEX_PAGE: "false" - sp3: - image: silintl/ssp-base:develop + ssp-sp3.local: + build: . volumes: # Utilize custom certs - ./development/sp3-local/cert:/data/vendor/simplesamlphp/simplesamlphp/cert @@ -334,7 +342,7 @@ services: # Utilize custom metadata - ./development/sp3-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php ports: - - "8083:80" + - "8084:80" env_file: - local.env environment: diff --git a/features/Sp1Idp1Sp2Idp2Sp3.feature b/features/Sp1Idp1Sp2Idp2Sp3.feature index 8d53769f..be765055 100644 --- a/features/Sp1Idp1Sp2Idp2Sp3.feature +++ b/features/Sp1Idp1Sp2Idp2Sp3.feature @@ -4,24 +4,23 @@ Feature: Ensure I can login to Sp1 through Idp1, must login to Sp2 through Idp2 When I go to the SP1 login page And the url should match "sildisco/disco.php" And I should see "to continue to SP1" - And I click on the "IdP 1" tile - And I login using password "a" - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP1 Scenario: After IDP1 login, go to SP2 through IDP2 Given I have authenticated with IDP1 for SP1 When I go to the SP2 login page - And I should see "Enter your username and password" - And I login using password "b" - Then I should see "test_admin@idp2.org" + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 Scenario: After IDP1 login, go directly to SP3 without credentials Given I have authenticated with IDP1 for SP1 When I go to the SP3 login page And the url should match "sildisco/disco.php" And I should see "to continue to SP3" - And I click on the "IdP 1" tile - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + Then I should see my attributes on SP3 Scenario: Logout of IDP1 Given I have authenticated with IDP1 for SP1 diff --git a/features/Sp1Idp2Sp2Sp3Idp1.feature b/features/Sp1Idp2Sp2Sp3Idp1.feature index 9f83916f..aba78e0d 100644 --- a/features/Sp1Idp2Sp2Sp3Idp1.feature +++ b/features/Sp1Idp2Sp2Sp3Idp1.feature @@ -4,20 +4,19 @@ Feature: Ensure I can login to Sp1 through Idp2, am already logged in for Sp2, a When I go to the SP1 login page And the url should match "sildisco/disco.php" And I should see "to continue to SP1" - And I click on the "IdP 2" tile - And I login using password "b" - Then I should see "test_admin@idp2.org" + And I click on the "IDP 2" tile + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP1 Scenario: After IDP2 login, go directly to SP2 without credentials Given I have authenticated with IDP2 for SP1 When I go to the SP2 login page - Then I should see "test_admin@idp2.org" + Then I should see my attributes on SP2 Scenario: After IDP2 login, go to SP3 through IDP1 Given I have authenticated with IDP2 for SP1 When I go to the SP3 login page And I should see "to continue to SP3" - And I click on the "IdP 1" tile - And I should see "Enter your username and password" - And I login using password "a" - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP3 diff --git a/features/Sp2Idp2Sp1Idp1Sp3.feature b/features/Sp2Idp2Sp1Idp1Sp3.feature index 24e6498c..c1637991 100644 --- a/features/Sp2Idp2Sp1Idp1Sp3.feature +++ b/features/Sp2Idp2Sp1Idp1Sp3.feature @@ -2,18 +2,16 @@ Feature: Ensure I can login to Sp2 through Idp2, must login to Sp1 if I choose I Scenario: Login to SP2 through IDP2 When I go to the SP2 login page - And I should see "Enter your username and password" - And I login using password "b" - Then I should see "test_admin@idp2.org" + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 Scenario: Login to SP1 through IDP1 Given I have authenticated with IDP2 for SP2 When I go to the SP1 login page And the url should match "sildisco/disco.php" - And I click on the "IdP 1" tile - And I should see "Enter your username and password" - And I login using password "a" - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP1 Scenario: After IDP2 login, go directly to SP3 without credentials Given I have authenticated with IDP2 for SP2 @@ -21,6 +19,6 @@ Feature: Ensure I can login to Sp2 through Idp2, must login to Sp1 if I choose I And I go to the SP3 login page And the url should match "sildisco/disco.php" And I should see "to continue to SP3" - And I click on the "IdP 1" tile - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + Then I should see my attributes on SP3 diff --git a/features/Sp2Idp2Sp1Idp2Sp3.feature b/features/Sp2Idp2Sp1Idp2Sp3.feature index c7e785ba..e54e3244 100644 --- a/features/Sp2Idp2Sp1Idp2Sp3.feature +++ b/features/Sp2Idp2Sp1Idp2Sp3.feature @@ -2,23 +2,21 @@ Feature: Ensure I can login to Sp2 through Idp2, get discovery page for Sp1, and Scenario: Login to SP2 through IDP2 When I go to the SP2 login page - And I should see "Enter your username and password" - And I login using password "b" - Then I should see "test_admin@idp2.org" + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 Scenario: Get discovery page for SP1 Given I have authenticated with IDP2 for SP2 When I go to the SP1 login page And the url should match "sildisco/disco.php" - And I click on the "IdP 2" tile - Then I should see "test_admin@idp2.org" + And I click on the "IDP 2" tile + Then I should see my attributes on SP1 Scenario: Must login to SP3 through IDP1 Given I have authenticated with IDP2 for SP2 When I go to the SP3 login page And the url should match "sildisco/disco.php" - And I click on the "IdP 1" tile - And I should see "Enter your username and password" - And I login using password "a" - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP3 diff --git a/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature b/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature index b7041e31..c038df65 100644 --- a/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature +++ b/features/Sp3Idp1Sp1Idp1Sp2Idp2.feature @@ -4,21 +4,19 @@ Feature: Ensure I can login to Sp3 through Idp1, get the discovery page for Sp1 When I go to the SP3 login page And the url should match "sildisco/disco.php" And I should see "to continue to SP3" - And I click on the "IdP 1" tile - And I should see "Enter your username and password" - And I login using password "a" - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + And I log in using my "IDP 1" credentials + Then I should see my attributes on SP3 Scenario: having authenticated with IDP1 for SP3, go to SP1 via the discovery page Given I have authenticated with IDP1 for SP3 When I go to the SP1 login page And the url should match "sildisco/disco.php" - And I click on the "IdP 1" tile - Then I should see "test_admin@idp1.org" + And I click on the "IDP 1" tile + Then I should see my attributes on SP1 Scenario: having authenticated with IDP1 for SP3, login to SP2 using IDP2 Given I have authenticated with IDP1 for SP3 When I go to the SP2 login page - And I should see "Enter your username and password" - And I login using password "b" - Then I should see "test_admin@idp2.org" + And I log in using my "IDP 2" credentials + Then I should see my attributes on SP2 diff --git a/features/WwwMetadataCept.feature b/features/WwwMetadataCept.feature index 0778c0e5..f8fb2eb4 100644 --- a/features/WwwMetadataCept.feature +++ b/features/WwwMetadataCept.feature @@ -1,13 +1,13 @@ Feature: Ensure I see the hub's metadata page. Scenario: Show the hub's metadata page in default format - When I go to "http://hub4tests/module.php/sildisco/metadata.php" - Then I should see "$metadata['hub4tests']" + When I go to "http://ssp-hub.local/module.php/sildisco/metadata.php" + Then I should see "$metadata['ssp-hub.local']" Scenario: Show the hub's metadata page in XML format - When I go to "http://hub4tests/module.php/sildisco/metadata.php?format=xml" + When I go to "http://ssp-hub.local/module.php/sildisco/metadata.php?format=xml" Then I should see the metadata in XML format Scenario: Show the hub's metadata page PHP format - When I go to "http://hub4tests/module.php/sildisco/metadata.php?format=php" - Then I should see "$metadata['hub4tests']" + When I go to "http://ssp-hub.local/module.php/sildisco/metadata.php?format=php" + Then I should see "$metadata['ssp-hub.local']" diff --git a/features/ZSp1Idp1BetaSp1Idp3.feature b/features/ZSp1Idp1BetaSp1Idp3.feature index c480ad1f..e82b9053 100644 --- a/features/ZSp1Idp1BetaSp1Idp3.feature +++ b/features/ZSp1Idp1BetaSp1Idp3.feature @@ -6,9 +6,8 @@ Scenario: Normally the IdP3 is disabled Then the "div" element should contain "IdP 3 coming soon" Scenario: After going to the "Beta Test" page, IdP3 is available for use - When I go to "http://hub4tests/module.php/sildisco/betatest.php" + When I go to "http://ssp-hub.local/module.php/sildisco/betatest.php" And I go to the "SP1" login page - And I click on the "IdP 3" tile - And I should see "Enter your username and password" - And I login using password "c" + And I click on the "IDP 3" tile + And I log in using my "IDP 3" credentials Then I should see "test_admin@idp3.org" diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 0dbf537e..748733c7 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -1,11 +1,15 @@ session->getStatusCode() . '] '; $this->printLastResponse(); @@ -210,4 +217,118 @@ public function theFileShouldContain($filePath, PyStringNode $expectedJson) json_decode($expectedJson, true) ); } + + /** + * Get the login button from the given page. + * + * @param DocumentElement $page The page. + * @return NodeElement + */ + protected function getLoginButton($page) + { + $buttons = $page->findAll('css', 'button'); + $loginButton = null; + foreach ($buttons as $button) { + $lcButtonText = strtolower($button->getText()); + if (strpos($lcButtonText, 'login') !== false) { + $loginButton = $button; + break; + } + } + Assert::notNull($loginButton, 'Failed to find the login button'); + return $loginButton; + } + + /** + * @When I log in + */ + public function iLogIn() + { + $page = $this->session->getPage(); + try { + $page->fillField('username', $this->username); + $page->fillField('password', $this->password); + $this->submitLoginForm($page); + } catch (ElementNotFoundException $e) { + Assert::true(false, sprintf( + "Did not find that element in the page.\nError: %s\nPage content: %s", + $e->getMessage(), + $page->getContent() + )); + } + } + + /** + * @Given I have logged in (again) + */ + public function iHaveLoggedIn() + { + $this->iLogin(); + } + + /** + * Submit the current form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported) by + * clicking the specified button. + * + * @param string $buttonName The value of the desired button's `name` + * attribute. + */ + protected function submitFormByClickingButtonNamed($buttonName) + { + $page = $this->session->getPage(); + $button = $page->find('css', sprintf( + '[name=%s]', + $buttonName + )); + Assert::notNull($button, 'Failed to find button named ' . $buttonName); + $button->click(); + $this->submitSecondarySspFormIfPresent($page); + } + + /** + * Submit the login form, including the secondary page's form (if + * simpleSAMLphp shows another page because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitLoginForm($page) + { + $loginButton = $this->getLoginButton($page); + $loginButton->click(); + $this->submitSecondarySspFormIfPresent($page); + } + + /** + * Submit the secondary page's form (if simpleSAMLphp shows another page + * because JavaScript isn't supported). + * + * @param DocumentElement $page The page. + */ + protected function submitSecondarySspFormIfPresent($page) + { + // SimpleSAMLphp 1.15 markup for secondary page: + $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); + if ($postLoginSubmitButton instanceof NodeElement) { + $postLoginSubmitButton->click(); + } else { + + // SimpleSAMLphp 1.14 markup for secondary page: + $body = $page->find('css', 'body'); + if ($body instanceof NodeElement) { + $onload = $body->getAttribute('onload'); + if ($onload === "document.getElementsByTagName('input')[0].click();") { + $body->pressButton('Submit'); + } + } + } + } + + /** + * @Then I should end up at my intended destination + */ + public function iShouldEndUpAtMyIntendedDestination() + { + $this->assertPageBodyContainsText('Your attributes'); + } } diff --git a/features/bootstrap/SilDiscoContext.php b/features/bootstrap/SilDiscoContext.php index cfc9e4a0..24781840 100644 --- a/features/bootstrap/SilDiscoContext.php +++ b/features/bootstrap/SilDiscoContext.php @@ -1,7 +1,49 @@ username = 'sildisco_idp1'; + $this->password = 'sildisco_password'; + break; + + case 'IDP 2': + $this->username = 'sildisco_idp2'; + $this->password = 'sildisco_password'; + break; + + case 'IDP 3': + $this->username = 'admin'; + $this->password = 'c'; + break; + + default: + throw new \Exception('credential name not recognized'); + } + $this->iLogIn(); + } + + /** + * @Then I should see my attributes on :sp + */ + public function iShouldSeeMyAttributesOnSp($sp) + { + $currentUrl = $this->session->getCurrentUrl(); + Assert::assertStringStartsWith( + 'http://ssp-' . strtolower($sp), + $currentUrl, + 'Did NOT end up at ' . $sp + ); + $this->assertPageContainsText('Your attributes'); + } + /** * @When I login using password :password */ @@ -16,8 +58,10 @@ public function iLoginUsingPassword($password) public function iHaveAuthenticatedWithIdp1($sp) { $this->iGoToTheSpLoginPage($sp); - $this->iClickOnTheTile('IdP 1'); - $this->logInAs('admin', 'a'); + $this->iClickOnTheTile('IDP 1'); + $this->username = 'sildisco_idp1'; + $this->password = 'sildisco_password'; + $this->iLogIn(); } /** @@ -27,9 +71,11 @@ public function iHaveAuthenticatedWithIdp2($sp) { $this->iGoToTheSpLoginPage($sp); if ($sp != "SP2") { // SP2 only has IDP2 in its IDPList - $this->iClickOnTheTile('IdP 2'); + $this->iClickOnTheTile('IDP 2'); } - $this->logInAs('admin', 'b'); + $this->username = 'sildisco_idp2'; + $this->password = 'sildisco_password'; + $this->iLogIn(); } /** @@ -38,7 +84,7 @@ public function iHaveAuthenticatedWithIdp2($sp) public function iLogOutOfIdp1() { $this->iGoToTheSpLoginPage('SP3'); - $this->iClickOnTheTile('IdP 1'); + $this->iClickOnTheTile('IDP 1'); $this->clickLink('Logout'); $this->assertPageContainsText('You have been logged out.'); } @@ -58,8 +104,17 @@ public function iLogOutOfIdp2() */ public function iShouldSeeTheMetadataInXmlFormat() { - $xml = $this->getSession()->getDriver()->getContent(); - assert(str_contains($xml, 'entityID="hub4tests"')); + $contentType = $this->session->getResponseHeader('Content-Type'); + Assert::assertEquals('application/xml', $contentType); + + Assert::assertEquals(200, $this->session->getStatusCode()); + + $xml = file_get_contents($this->getSession()->getCurrentUrl()); + Assert::assertStringContainsString( + 'entityID="ssp-hub.local"', + $xml, + "page doesn't contain entityID" + ); } } From 3fdefd159387e8fe2ac1bc99fe441546b6a1ab67 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 20 May 2024 16:31:58 +0800 Subject: [PATCH 58/92] remove duplicate code from individual context classes --- features/bootstrap/ExpiryContext.php | 70 ------------- features/bootstrap/LoginContext.php | 6 -- features/bootstrap/MfaContext.php | 108 +------------------- features/bootstrap/ProfileReviewContext.php | 101 +----------------- features/expirychecker.feature | 10 +- features/mfa.feature | 32 +++--- features/profilereview.feature | 4 +- 7 files changed, 28 insertions(+), 303 deletions(-) diff --git a/features/bootstrap/ExpiryContext.php b/features/bootstrap/ExpiryContext.php index 473ff18b..749c134b 100644 --- a/features/bootstrap/ExpiryContext.php +++ b/features/bootstrap/ExpiryContext.php @@ -9,9 +9,6 @@ */ class ExpiryContext extends FeatureContext { - protected $username = null; - protected $password = null; - /** * The browser session, used for interacting with the website. * @@ -64,27 +61,6 @@ protected function assertFormNotContains($text, $page) } } - /** - * Get the login button from the given page. - * - * @param DocumentElement $page The page. - * @return NodeElement - */ - protected function getLoginButton($page) - { - $buttons = $page->findAll('css', 'button'); - $loginButton = null; - foreach ($buttons as $button) { - $lcButtonText = strtolower($button->getText()); - if (strpos($lcButtonText, 'login') !== false) { - $loginButton = $button; - break; - } - } - Assert::assertNotNull($loginButton, 'Failed to find the login button'); - return $loginButton; - } - /** * @Given I provide credentials that will expire in the distant future */ @@ -95,24 +71,6 @@ public function iProvideCredentialsThatWillExpireInTheDistantFuture() $this->password = 'a'; } - /** - * @When I login - */ - public function iLogin() - { - $this->fillField('username', $this->username); - $this->fillField('password', $this->password); - $this->pressButton('Login'); - } - - /** - * @Then I should end up at my intended destination - */ - public function iShouldEndUpAtMyIntendedDestination() - { - $this->assertPageBodyContainsText('Your attributes'); - } - /** * @Given I provide credentials that will expire very soon */ @@ -132,34 +90,6 @@ public function iShouldSeeAWarningThatMyPasswordWillExpireSoon() Assert::assertContains('will expire', $page->getHtml()); } - /** - * Submit the login form, including the secondary page's form (if - * simpleSAMLphp shows another page because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitLoginForm($page) - { - $loginButton = $this->getLoginButton($page); - $loginButton->click(); - - // SimpleSAMLphp 1.15 markup for secondary page: - $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); - if ($postLoginSubmitButton instanceof NodeElement) { - $postLoginSubmitButton->click(); - } else { - - // SimpleSAMLphp 1.14 markup for secondary page: - $body = $page->find('css', 'body'); - if ($body instanceof NodeElement) { - $onload = $body->getAttribute('onload'); - if ($onload === "document.getElementsByTagName('input')[0].click();") { - $body->pressButton('Submit'); - } - } - } - } - /** * @Then there should be a way to go change my password now */ diff --git a/features/bootstrap/LoginContext.php b/features/bootstrap/LoginContext.php index 61cc2e39..7425e2b7 100644 --- a/features/bootstrap/LoginContext.php +++ b/features/bootstrap/LoginContext.php @@ -37,15 +37,9 @@ class LoginContext extends FeatureContext /** @var IdBroker */ private $idBroker; - /** @var string|null */ - private $password = null; - /** @var Request */ private $request; - /** @var string|null */ - private $username = null; - /** * Initializes context. * diff --git a/features/bootstrap/MfaContext.php b/features/bootstrap/MfaContext.php index 78600561..16dbdaa0 100644 --- a/features/bootstrap/MfaContext.php +++ b/features/bootstrap/MfaContext.php @@ -12,9 +12,6 @@ */ class MfaContext extends FeatureContext { - protected $username = null; - protected $password = null; - const USER_AGENT_WITHOUT_WEBAUTHN_SUPPORT = 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko'; const USER_AGENT_WITH_WEBAUTHN_SUPPORT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36'; @@ -51,28 +48,7 @@ protected function getContinueButton($page) $continueButton = $page->find('css', '[name=continue]'); return $continueButton; } - - /** - * Get the login button from the given page. - * - * @param DocumentElement $page The page. - * @return NodeElement - */ - protected function getLoginButton($page) - { - $buttons = $page->findAll('css', 'button'); - $loginButton = null; - foreach ($buttons as $button) { - $lcButtonText = strtolower($button->getText()); - if (strpos($lcButtonText, 'login') !== false) { - $loginButton = $button; - break; - } - } - Assert::assertNotNull($loginButton, 'Failed to find the login button'); - return $loginButton; - } - + /** * Get the button for submitting the MFA form. * @@ -85,35 +61,7 @@ protected function getSubmitMfaButton($page) Assert::assertNotNull($submitMfaButton, 'Failed to find the submit-MFA button'); return $submitMfaButton; } - - /** - * @When I login - */ - public function iLogin() - { - $page = $this->session->getPage(); - try { - $page->fillField('username', $this->username); - $page->fillField('password', $this->password); - $this->submitLoginForm($page); - } catch (ElementNotFoundException $e) { - Assert::fail(sprintf( - "Did not find that element in the page.\nError: %s\nPage content: %s", - $e->getMessage(), - $page->getContent() - )); - } - } - - /** - * @Then I should end up at my intended destination - */ - public function iShouldEndUpAtMyIntendedDestination() - { - $page = $this->session->getPage(); - Assert::assertContains('Your attributes', $page->getHtml()); - } - + /** * Submit the current form, including the secondary page's form (if * simpleSAMLphp shows another page because JavaScript isn't supported) by @@ -133,21 +81,7 @@ protected function submitFormByClickingButtonNamed($buttonName) $button->click(); $this->submitSecondarySspFormIfPresent($page); } - - /** - * Submit the login form, including the secondary page's form (if - * simpleSAMLphp shows another page because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitLoginForm($page) - { - $loginButton = $this->getLoginButton($page); - $loginButton->click(); - $this->submitSecondarySspFormIfPresent($page); - } - - + /** * Submit the MFA form, including the secondary page's form (if * simpleSAMLphp shows another page because JavaScript isn't supported). @@ -161,33 +95,7 @@ protected function submitMfaForm($page) $this->submitSecondarySspFormIfPresent($page); } - - /** - * Submit the secondary page's form (if simpleSAMLphp shows another page - * because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitSecondarySspFormIfPresent($page) - { - // SimpleSAMLphp 1.15 markup for secondary page: - $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); - if ($postLoginSubmitButton instanceof NodeElement) { - $postLoginSubmitButton->click(); - } else { - - // SimpleSAMLphp 1.14 markup for secondary page: - $body = $page->find('css', 'body'); - if ($body instanceof NodeElement) { - $onload = $body->getAttribute('onload'); - if ($onload === "document.getElementsByTagName('input')[0].click();") { - $body->pressButton('Submit'); - } - } - } - } - - /** + /** * @Given I provide credentials that do not need MFA */ public function iProvideCredentialsThatDoNotNeedMfa() @@ -286,14 +194,6 @@ public function iShouldSeeAPromptForAWebAuthn() Assert::assertContains('

USB Security Key

', $page->getHtml()); } - /** - * @Given I have logged in (again) - */ - public function iHaveLoggedIn() - { - $this->iLogin(); - } - protected function submitMfaValue($mfaValue) { $page = $this->session->getPage(); diff --git a/features/bootstrap/ProfileReviewContext.php b/features/bootstrap/ProfileReviewContext.php index 8e60c0d3..9c88f526 100644 --- a/features/bootstrap/ProfileReviewContext.php +++ b/features/bootstrap/ProfileReviewContext.php @@ -10,11 +10,6 @@ */ class ProfileReviewContext extends FeatureContext { - protected $nonPwManagerUrl = 'http://sp/module.php/core/authenticate.php?as=profilereview-idp-no-port'; - - protected $username = null; - protected $password = null; - /** * Assert that the given page has a form that contains the given text. * @@ -37,55 +32,6 @@ protected function assertFormContains($text, $page) )); } - /** - * Get the login button from the given page. - * - * @param DocumentElement $page The page. - * @return NodeElement - */ - protected function getLoginButton($page) - { - $buttons = $page->findAll('css', 'button'); - $loginButton = null; - foreach ($buttons as $button) { - $lcButtonText = strtolower($button->getText()); - if (strpos($lcButtonText, 'login') !== false) { - $loginButton = $button; - break; - } - } - Assert::assertNotNull($loginButton, 'Failed to find the login button'); - return $loginButton; - } - - /** - * @When I login - */ - public function iLogin() - { - $page = $this->session->getPage(); - try { - $page->fillField('username', $this->username); - $page->fillField('password', $this->password); - $this->submitLoginForm($page); - } catch (ElementNotFoundException $e) { - Assert::fail(sprintf( - "Did not find that element in the page.\nError: %s\nPage content: %s", - $e->getMessage(), - $page->getContent() - )); - } - } - - /** - * @Then I should end up at my intended destination - */ - public function iShouldEndUpAtMyIntendedDestination() - { - $page = $this->session->getPage(); - Assert::assertContains('Your attributes', $page->getHtml()); - } - /** * Submit the current form, including the secondary page's form (if * simpleSAMLphp shows another page because JavaScript isn't supported) by @@ -105,45 +51,7 @@ protected function submitFormByClickingButtonNamed($buttonName) $button->click(); $this->submitSecondarySspFormIfPresent($page); } - - /** - * Submit the login form, including the secondary page's form (if - * simpleSAMLphp shows another page because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitLoginForm($page) - { - $loginButton = $this->getLoginButton($page); - $loginButton->click(); - $this->submitSecondarySspFormIfPresent($page); - } - - /** - * Submit the secondary page's form (if simpleSAMLphp shows another page - * because JavaScript isn't supported). - * - * @param DocumentElement $page The page. - */ - protected function submitSecondarySspFormIfPresent($page) - { - // SimpleSAMLphp 1.15 markup for secondary page: - $postLoginSubmitButton = $page->findButton('postLoginSubmitButton'); - if ($postLoginSubmitButton instanceof NodeElement) { - $postLoginSubmitButton->click(); - } else { - - // SimpleSAMLphp 1.14 markup for secondary page: - $body = $page->find('css', 'body'); - if ($body instanceof NodeElement) { - $onload = $body->getAttribute('onload'); - if ($onload === "document.getElementsByTagName('input')[0].click();") { - $body->pressButton('Submit'); - } - } - } - } - + /** * @Given I provide credentials that do not need review */ @@ -176,13 +84,6 @@ public function iProvideCredentialsThatAreDueForAReminder($category, $nagType) } } - /** - * @Given I have logged in (again) - */ - public function iHaveLoggedIn() - { - $this->iLogin(); - } protected function pageContainsElementWithText($cssSelector, $text) { diff --git a/features/expirychecker.feature b/features/expirychecker.feature index c690015c..f93f7af4 100644 --- a/features/expirychecker.feature +++ b/features/expirychecker.feature @@ -5,29 +5,29 @@ Feature: Expiry Checker module Scenario: Password will expire in the distant future Given I provide credentials that will expire in the distant future - When I login + When I log in Then I should end up at my intended destination Scenario: Password will expire tomorrow Given I provide credentials that will expire very soon - When I login + When I log in Then I should see a warning that my password will expire soon And there should be a way to go change my password now And there should be a way to continue without changing my password Scenario: Password has expired Given I provide credentials that have expired - When I login + When I log in Then I should see a message that my password has expired And there should be a way to go change my password now But there should NOT be a way to continue without changing my password Scenario: Reject missing expiration date Given I provide credentials that have no password expiration date - When I login + When I log in Then I should see an error message Scenario: Reject invalid expiration date Given I provide credentials that have an invalid password expiration date - When I login + When I log in Then I should see an error message diff --git a/features/mfa.feature b/features/mfa.feature index 1ccdba3a..5a9c948f 100644 --- a/features/mfa.feature +++ b/features/mfa.feature @@ -6,37 +6,37 @@ Feature: Prompt for MFA credentials Scenario: Don't prompt for MFA Given I provide credentials that do not need MFA - When I login + When I log in Then I should end up at my intended destination Scenario: Needs MFA, but no MFA options are available Given I provide credentials that need MFA but have no MFA options available - When I login + When I log in Then I should see a message that I have to set up MFA And there should be a way to go set up MFA now And there should NOT be a way to continue to my intended destination Scenario: Following the requirement to go set up MFA Given I provide credentials that need MFA but have no MFA options available - And I login + And I log in When I click the set-up-MFA button Then I should end up at the mfa-setup URL And I should NOT be able to get to my intended destination Scenario: Needs MFA, has backup code option available Given I provide credentials that need MFA and have backup codes available - When I login + When I log in Then I should see a prompt for a backup code Scenario: Needs MFA, has TOTP option available Given I provide credentials that need MFA and have TOTP available - When I login + When I log in Then I should see a prompt for a TOTP code Scenario: Needs MFA, has WebAuthn option available Given I provide credentials that need MFA and have WebAuthn available And the user's browser supports WebAuthn - When I login + When I log in Then I should see a prompt for a WebAuthn security key Scenario: Accepting a (non-rate-limited) correct MFA value @@ -129,7 +129,7 @@ Feature: Prompt for MFA credentials Scenario Outline: Defaulting to another option when WebAuthn is not supported Given I provide credentials that have And the user's browser - When I login + When I log in Then I should see a prompt for a Examples: @@ -155,7 +155,7 @@ Feature: Prompt for MFA credentials Given I provide credentials that have a used And and I have a more recently used And the user's browser - When I login + When I log in Then I should see a prompt for a Examples: @@ -170,13 +170,13 @@ Feature: Prompt for MFA credentials Scenario: Defaulting to the manager code despite having a used mfa Given I provide credentials that have a manager code, a WebAuthn and a more recently used TOTP And the user's browser supports WebAuthn - When I login + When I log in Then I should see a prompt for a manager rescue code Scenario Outline: When to show the WebAuthn-not-supported error message Given I provide credentials that have WebAuthn And the user's browser - When I login + When I log in Then I see an error message about WebAuthn being unsupported Examples: @@ -188,7 +188,7 @@ Feature: Prompt for MFA credentials Scenario Outline: When to show the link to send a manager rescue code Given I provide credentials that have And the user a manager email - When I login + When I log in Then I see a link to send a code to the user's manager Examples: @@ -211,13 +211,13 @@ Feature: Prompt for MFA credentials Scenario: Ask for a code to be sent to my manager Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in When I click the Request Assistance link Then there should be a way to request a manager code Scenario: Submit a code sent to my manager at an earlier time Given I provide credentials that have a manager code - And I login + And I log in When I submit the correct manager code # because profile review is required after using a manager code: And I click the remind-me-later button @@ -226,7 +226,7 @@ Feature: Prompt for MFA credentials Scenario: Submit a correct manager code Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in And I click the Request Assistance link And I click the Send a code link When I submit the correct manager code @@ -237,7 +237,7 @@ Feature: Prompt for MFA credentials Scenario: Submit an incorrect manager code Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in And I click the Request Assistance link And I click the Send a code link When I submit an incorrect manager code @@ -246,7 +246,7 @@ Feature: Prompt for MFA credentials Scenario: Ask for assistance, but change my mind Given I provide credentials that have backup codes And the user has a manager email - And I login + And I log in And I click the Request Assistance link When I click the Cancel button Then I should see a prompt for a backup code diff --git a/features/profilereview.feature b/features/profilereview.feature index b378e712..9c3988d1 100644 --- a/features/profilereview.feature +++ b/features/profilereview.feature @@ -5,12 +5,12 @@ Feature: Prompt to review profile information Scenario: Don't ask for review Given I provide credentials that do not need review - When I login + When I log in Then I should end up at my intended destination Scenario Outline: Present reminder as required by the user profile Given I provide credentials that are due for a reminder - When I login + When I log in Then I should see the message: And there should be a way to go update my profile now And there should be a way to continue to my intended destination From 15d53d4359540ea10edc89df1983edab7e382f85 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 20 May 2024 17:02:10 +0800 Subject: [PATCH 59/92] add metadata entries to satisfy the metadata tests --- development/hub/metadata/idp-remote.php | 2 +- development/hub/metadata/sp-remote.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/development/hub/metadata/idp-remote.php b/development/hub/metadata/idp-remote.php index a9ba60fa..56ecd8d6 100644 --- a/development/hub/metadata/idp-remote.php +++ b/development/hub/metadata/idp-remote.php @@ -76,7 +76,7 @@ 'certData' => 'MIIDzzCCAregAwIBAgIJALBaUrvz1X5DMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE4MTQwMDUxWhcNMjYxMDE4MTQwMDUxWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5mZNwjEnakJho+5etuFyx+2g9rs96iLX/LDC24aBAsdNxTNuIc1jJ7pxBxGrepEND4LkietLNBlOr1q50nq2+ddTrCfmoJB+9BqBOxcm9qWeqWbp8/arUjaxPzK3DfZrxJxIVFjzqFF7gI91y9yvEW/fqLRMhvnH1ns+N1ne59zr1y6h9mmHfBffGr1YXAfyEAuV1ich4AfTfjqhdwFwxhFLLCVnxA0bDbNw/0eGCSiA13N7a013xTurLeJu0AQaZYssMqvc/17UphH4gWDMEZAwy0EfRSBOsDOYCxeNxVajnWX1834VDpBDfpnZj996Gh8tzRQxQgT9/plHKhGiwIDAQABo1AwTjAdBgNVHQ4EFgQUApxlUQg26GrG3eH8lEG3SkqbH/swHwYDVR0jBBgwFoAUApxlUQg26GrG3eH8lEG3SkqbH/swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANhbm8WgIqBDlF7DIRVUbq04TEA9nOJG8wdjJYdoKrPX9f/E9slkFuD2StcK99RTcowa8Z2OmW7tksa+onyH611Lq21QXh4aHzQUAm2HbsmPQRZnkByeYoCJ/1tuEho+x+VGanaUICSBVWYiebAQVKHR6miFypRElibNBizm2nqp6Q9B87V8COzyDVngR1DlWDduxYaNOBgvht3Rk9Y2pVHqym42dIfN+pprcsB1PGBkY/BngIuS/aqTENbmoC737vcb06e8uzBsbCpHtqUBjPpL2psQZVJ2Y84JmHafC3B7nFQrjdZBbc9eMHfPo240Rh+pDLwxdxPqRAZdeLaUkCQ==', // limit which Sps can use this IdP - 'SPList' => ['http://ssp-sp1.local', 'http://ssp-sp2.local'], + 'SPList' => ['http://ssp-sp1.local:8081', 'http://ssp-sp2.local:8082'], ], 'http://ssp-idp2.local' => [ 'metadata-set' => 'saml20-idp-remote', diff --git a/development/hub/metadata/sp-remote.php b/development/hub/metadata/sp-remote.php index a45271e7..68f6974e 100644 --- a/development/hub/metadata/sp-remote.php +++ b/development/hub/metadata/sp-remote.php @@ -14,6 +14,11 @@ 'AssertionConsumerService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-acs.php/ssp-hub-custom-port', 'SingleLogoutService' => 'http://ssp-sp1.local:8081/module.php/saml/sp/saml2-logout.php/ssp-hub-custom-port', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', + 'IDPList' => [ + 'http://ssp-idp1.local:8085', + 'http://ssp-idp2.local:8086', + 'http://ssp-idp3.local:8087', + ], 'assertion.encryption' => true, ], @@ -22,6 +27,11 @@ 'AssertionConsumerService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-acs.php/ssp-hub', 'SingleLogoutService' => 'http://ssp-sp1.local/module.php/saml/sp/saml2-logout.php/ssp-hub', 'certData' => 'MIIDzzCCAregAwIBAgIJAPnOHgSgAeNrMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIyNzU2WhcNMjYxMDE3MTIyNzU2WjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0u+mXWS8vUkKjtJcK1hd0iGW2vbTvYosgyDdqClcSzwpbWJg1A1ChuiQIf7S+5bWL2AN4zMoem/JTn7cE9octqU34ZJAyP/cesppA9G53F9gH4XdoPgnWsb8vdWooDDUk+asc7ah/XwKixQNcELPDZkOba5+pqoKGjMxfL7JQ6+P6LB+xItzvLBXU4+onbGPIF6pmZ8S74mt0J62Y6ne40BHx8FdrtBgdk5TFcDedW09rRJrTFpi3hGSUkcjqj84B+oLAb08Z0SHoELMp5Yh7Tg5QZ2c+S8I47tQjV72rNhUYhIyFuImzSg27R7aRJ6Jj6sK4zEg0Ai4VhO4RmgyzwIDAQABo1AwTjAdBgNVHQ4EFgQUgkYcMbT0o8kmxAz2O3+p1lDVj1MwHwYDVR0jBBgwFoAUgkYcMbT0o8kmxAz2O3+p1lDVj1MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANgyTgMVRghgL8klqvZvQpfh80XDPTZotJCc8mZJZ98YkNC8jnR2RIUJpah+XrgotlKNDOK3HMNuyKGgYcqcno4PdDXKbqp4yXmywdNbbEHwPWDGqZXULw2az+UVwPUZJcJyJuwJjy3diCJT53N9G0LqXfeEsV0OPQPaB2PWgYNraBd59fckmBTc298HuvsHtxUcoXM53ms2Ck6GygGwH1vCg7qyIRRQFL4DiSlnoS8jxt3IIpZZs9FAl1ejtFBepSne9kEo7lLhAWY1TQqRrRXNHngG/L70ZkZonE9TNK/9xIHuaawqWkV6WLnkhT0DHCOw67GP97MWzceyFw+n9Vg==', + 'IDPList' => [ + 'http://ssp-idp1.local', + 'http://ssp-idp2.local', + 'http://ssp-idp3.local', + ], 'assertion.encryption' => true, ], From a0b9cfb0abe8378671e04f3508651372ef6286a3 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 20 May 2024 20:43:02 +0800 Subject: [PATCH 60/92] include IdpDiscoTest.php from simplesamlphp-module-sildisco https://github.com/silinternational/simplesamlphp-module-sildisco --- tests/IdpDiscoTest.php | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/IdpDiscoTest.php diff --git a/tests/IdpDiscoTest.php b/tests/IdpDiscoTest.php new file mode 100644 index 00000000..df5e171a --- /dev/null +++ b/tests/IdpDiscoTest.php @@ -0,0 +1,52 @@ +assertEquals($expected, $results); + } + + public function testEnableBetaEnabledNoChange() + { + $isBetaEnabled = 1; + $enabledKey = IdPDisco::$enabledMdKey; + $idpList = [ + 'idp1' => [$enabledKey => false], + 'idp2' => [$enabledKey => true], + ]; + $expected = $idpList; + + $results = IdPDisco::enableBetaEnabled($idpList, $isBetaEnabled); + $this->assertEquals($expected, $results); + } + + public function testEnableBetaEnabledChange() + { + $isBetaEnabled = 1; + $enabledKey = IdPDisco::$enabledMdKey; + $betaEnabledKey = IdPDisco::$betaEnabledMdKey; + $idpList = [ + 'idp1' => [$enabledKey => false], + 'idp2' => [$enabledKey => true, $betaEnabledKey => true], + 'idp3' => [$enabledKey => false, $betaEnabledKey => true], + 'idp4' => [$enabledKey => false, $betaEnabledKey => false], + ]; + $expected = $idpList; + $expected['idp3'][$enabledKey] = true; + + $results = IdPDisco::enableBetaEnabled($idpList, $isBetaEnabled); + $this->assertEquals($expected, $results); + } + +} From 69fa5e0b081883358a615fcb263683ca865480cf Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 21 May 2024 13:35:11 +0800 Subject: [PATCH 61/92] copy markdown files from the sildisco Wiki --- README.md | 26 ++++++++ docs/development.md | 17 +++++ docs/editing_authprocs.md | 40 ++++++++++++ docs/functional_testing.md | 129 +++++++++++++++++++++++++++++++++++++ docs/overview.md | 4 ++ docs/the_hub.md | 41 ++++++++++++ 6 files changed, 257 insertions(+) create mode 100644 docs/development.md create mode 100644 docs/editing_authprocs.md create mode 100644 docs/functional_testing.md create mode 100644 docs/overview.md create mode 100644 docs/the_hub.md diff --git a/README.md b/README.md index 2fd91468..f920c616 100644 --- a/README.md +++ b/README.md @@ -375,3 +375,29 @@ load balancer) in the TRUSTED_IP_ADDRESSES environment variable (see #### Status Check To check the status of the website, you can access this URL: `https://(your domain name)/module.php/silauth/status.php` + +### SilDisco module for SAML Discovery + +#### Configuration + +Ensure the DYNAMO_* environment variables are set as shown in the local.env.dist file. + +#### Overview + +[Module Overview](./docs/overview.md) + +#### The Hub + +[The Hub](./docs/the_hub.md) + +#### Authprocs + +[Editing Authprocs](./docs/editing_authprocs.md) + +#### Development + +[Development](./docs/development.md) + +#### Functional Testing + +[Functional Testing](./docs/functional_testing.md) diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 00000000..bff583dc --- /dev/null +++ b/docs/development.md @@ -0,0 +1,17 @@ +Three SPs, a hub (a combined IdP and SP) and three IdPs get spun up by docker-compose. In order for this to work, you will need to edit your hosts file to include entries for the following domains ... +* ssp-sp1.local # to be used with port 8081 +* ssp-sp2.local # to be used with port 8082 +* ssp-sp3.local # to be used with port 8084 +* ssp-hub.local +* ssp-idp1.local # to be used with port 8085 +* ssp-idp2.local # to be used with port 8086 +* ssp-idp3.local # to be used with port 8087 + +The ./development folder holds various files needed by these containers. It's the ssp-hub.local container which is the focus and serves as the SimpleSAMLphp hub. + +### Who should see what? +* `ssp-sp1.local` should be able to see and authenticate through both `ssp-idp1.local` and `ssp-idp2.local` +* `ssp-sp2.local` should only be able to see and authenticate through `ssp-idp2.local` +* `ssp-sp3.local` should only be able to see and authenticate through `ssp-idp1.local` + +If a session authenticated through one of the IdP's that is not permitted for a certain SP, then the hub should force that SP to re-authenticate against the right IdP. diff --git a/docs/editing_authprocs.md b/docs/editing_authprocs.md new file mode 100644 index 00000000..42a62368 --- /dev/null +++ b/docs/editing_authprocs.md @@ -0,0 +1,40 @@ +The sildisco module includes a few Auth Procs that can be called from the `config.php` file or **SP or IdP metadata**. + +### AttributeMap.php + +Copies (rather than replaces) attributes according to an attribute map. + +### TagGroup.php + +Grabs the values of the `urn:oid:2.5.4.31` (member of) attribute and prepends them with `idp||`. +The idp's name value is taken from the saml20-idp-remote.php file. In particular, if the IdP's metadata entry includes a `'IDPNamespace'` value, that is used. Otherwise, if it includes a `'name'` value, that is used. Otherwise, it uses the entity id of the IdP. + +### AddIdp2NameId.php + +Grabs the value of the saml:sp:NameID and appends `@` to it. +The IdP's metadata needs to include an `'IDPNamespace'` entry with a string value that is alphanumeric with hyphens and underscores. + +In order for this to work, the SP needs to include a line in its authsources.php file in the Hub's entry ... + +` 'NameIDPolicy' => "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",` + +In addition, the IDP's sp-remote metadata stanza for the Hub needs to include ... + +` 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',` + +### TrackIdps.php + +Creates and/or appends to a session value ("sildisco:authentication", "authenticated_idps") the **entity id** of the latest **IdP** to be used for authentication. + +### LogUser.php + +Logs information (common name, eduPrincipalPersonalName, employee number, IdP, SP, time) about each successful login to an AWS Dynamodb table. +``` + 97 => [ + 'class' =>'sildisco:LogUser', + 'DynamoRegion' => 'us-east-1', + 'DynamoLogTable' => 'sildisco_prod_user-log', + ], +``` +The following config is not needed on AWS, but it is needed locally +'DynamoEndpoint' ex. http://dynamo:8000 diff --git a/docs/functional_testing.md b/docs/functional_testing.md new file mode 100644 index 00000000..651e216b --- /dev/null +++ b/docs/functional_testing.md @@ -0,0 +1,129 @@ +# Automated Testing + +This is done through behat acceptance tests + +Once your containers are up, in your VM run ... + +`> docker-compose run --rm test /data/run-integration-tests.sh` + +Or, if you need to run just one of the tests, run ... + +`> docker-compose run --rm test bash` + +then + +`$ vendor/bin/behat features/mfa.feature:7` + +The tests are found in `/features`. They are similar to the manual tests listed below. + +# Manual Testing +## Main SP authenticates through Main Idp. Third SP is also authenticated. Second SP must re-authenticate. +### Ensure main SP goes to discovery page and can login through the main IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 1 +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password (without the quotation marks). +* This should return you to the main SP at http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure third SP is also authenticated +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure second SP is forced to authenticate +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure third SP is still authenticated +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +## Second SP authenticates through Second Idp. Main SP is forced to discovery page but is also authenticated. Third SP must re-authenticate. +### Ensure second SP can login through the second IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure main SP goes to discovery page but is authenticated +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 2 +* This should return you to the main SP at http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure third SP is forced to authenticate +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +## Third SP authenticates through Main Idp. Main SP is forced to discovery page but is also authenticated. Second SP must re-authenticate. +### Ensure third SP can login through the main IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure main SP goes to discovery page but is authenticated +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 1 +* This should get you to http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure second SP is forced to authenticate +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +## Main SP authenticates through Second Idp. Second SP is also authenticated. Third SP must re-authenticate. +### Ensure main SP goes to discovery page and can login through the second IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 2 +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password +* This should return you to the main SP at http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure second SP is also authenticated +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure third SP is forced to authenticate +* Browse to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +### Ensure second SP is still authenticated +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. + +## Second SP authenticates through Second Idp. Main SP is forced to discovery page, chooses main IdP and must authenticate. +### Ensure second SP can login through the second IdP +* Kill all your cookies for ssp\* +* Browse to http://ssp-sp2.local:8082/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-idp2.local:8086/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "b" as the password. +* This should get you to http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes (but there are none). + +### Ensure main SP goes to discovery page and must authenticate when choosing the main Idp +* Browse to http://ssp-sp1.local:8081/module.php/core/authenticate.php +* Click on ssp-hub +* This should redirect to http://ssp-hub.local/module.php/sildisco/disco.php?entityID=ssp-hub.local&... +* Select IdP 1 +* This should redirect to http://ssp-idp1.local:8085/module.php/core/loginuserpass.php?AuthState=... +* Login as admin using "a" as the password. +* This should get you to http://ssp-sp3.local:8083/module.php/core/authenticate.php?as=ssp-hub and show your saml attributes. diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 00000000..8e36676b --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,4 @@ +The sildisco module is a module for simplesamlphp. It's main purpose is to allow a simplesamlphp hub to control which Service Providers (SP's) can see and authenticate through which Identity Providers (IdP's). + +It relies on some utilities found in [ssp-utilities](https://github.com/silinternational/ssp-utilities). + diff --git a/docs/the_hub.md b/docs/the_hub.md new file mode 100644 index 00000000..d1c4b588 --- /dev/null +++ b/docs/the_hub.md @@ -0,0 +1,41 @@ +The hub will need its certs, `config.php` and `authsources.php` files as a normal simplesamlphp installation. Examples of these can be found in the `./development/hub` folder. (Note the `discoURL` entry in the `authsources.php` file.) + +Other files it will need are as follows ... +* `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/default-enable` needs to be created (just an empty file) +* The files in the `./lib` folder will need to go into `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/lib` +* The files in the `./www` folder will need to go into `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/www` +* The `./sspoverrides/www_saml2_idp/SSOService.php` file will need overwrite the same out-of-the-box file in `/data/vendor/simplesamlphp/simplesamlphp/www/saml2/idp/` + +### Metadata files +The hub should use the `saml20-*-remote.php` files from [ssp-base](https://github.com/silinternational/ssp-base) in `/data/vendor/simplesamlphp/simplesamlphp/metadata/`. These pull in metadata from all the files named `idp-*.php` and `sp-*.php` respectively, including those in sub-folders. + +In order for forced re-authentication to be limited only to situations which warrant it, the `saml20-idp-hosted.php` file should include an authproc as such ... +> [ +> 'class' =>'sildisco:TrackIdps', +> ] + +#### IDP Remote metadata + +##### IDPNamespace +Each metadata stanza should include an `IDPNamespace` entry that includes no special characters. This is intended for namespacing the `NameId` value in the Auth Proc `AddIdp2NameId.php`. +It is also used by the `TagGroup.php` Auth Proc to convert group names into the form ... + +`idp||`. + +##### betaEnabled +An optional metadata entry is `betaEnabled`. +This will allow the IdP to be marked as `'enable' => true` when the user has a certain cookie ('beta_tester') that they would get from visiting `hub_domain/module.php/sildisco/betatest.php`. +The user would need to manually remove that cookie to be free of this effect. + +Sildisco does not otherwise deal with looking at the `'enable'` value. However, a theme for idp discovery may (e.g. simplesamlphp-module-material). + +##### SPList +In order to limit access to an IdP to only certain SP's, add an `'SPList'` array entry to the metadata for the IdP. The values of this array should match the `entity_id` values from the `sp-remote.php` metadata. + +##### excludeByDefault +If you want to require SP's to list a certain IdP in their IDPList entry in order to be able to access it, add `excludeByDefault => true` to that IdP's metadata. + +### Forced IdP discovery +The `.../lib/IdP/SAML2.php` file ensures that if an SP is allowed to access more than one IdP, then the user will be forced back to the IdP discovery page, even if they are already authenticated through one of those IdP's. + +The reason for this is to ensure that the user has a chance to decide which of their identities is used for that SP. From 42160b1ca17b7fd58afc05e763e2bfe4379eea74 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 21 May 2024 14:29:55 +0800 Subject: [PATCH 62/92] copy content from the simplesamlphp-module-material repo https://github.com/silinternational/simplesamlphp-module-material --- README.md | 69 ++ docs/material_tests.md | 187 +++++ .../dictionaries/about2expire.definition.json | 45 ++ .../dictionaries/error.definition.json | 21 + .../dictionaries/expired.definition.json | 27 + .../dictionaries/footer.definition.json | 9 + .../dictionaries/login.definition.json | 62 ++ .../dictionaries/logout.definition.json | 21 + .../material/dictionaries/mfa.definition.json | 399 ++++++++++ .../material/dictionaries/nag.definition.json | 75 ++ .../dictionaries/review.definition.json | 75 ++ .../dictionaries/selectidp.definition.json | 38 + .../themes/material/common-announcement.php | 15 + .../themes/material/common-footer.php | 3 + .../themes/material/common-head-elements.php | 39 + .../themes/material/core/loginuserpass.php | 159 ++++ .../themes/material/default/error.php | 41 + .../themes/material/default/logout.php | 27 + .../material/default/selectidp-links.php | 124 +++ .../material/expirychecker/about2expire.php | 58 ++ .../themes/material/expirychecker/expired.php | 41 + .../material/mfa/low-on-backup-codes.php | 52 ++ .../themes/material/mfa/must-set-up-mfa.php | 48 ++ .../themes/material/mfa/new-backup-codes.php | 158 ++++ .../themes/material/mfa/other_mfas.php | 58 ++ .../material/mfa/out-of-backup-codes.php | 58 ++ .../mfa/prompt-for-mfa-backupcode.php | 92 +++ .../material/mfa/prompt-for-mfa-manager.php | 89 +++ .../material/mfa/prompt-for-mfa-totp.php | 92 +++ .../material/mfa/prompt-for-mfa-u2f.php | 175 ++++ .../material/mfa/prompt-for-mfa-webauthn.php | 157 ++++ .../themes/material/mfa/send-manager-mfa.php | 51 ++ .../material/profilereview/nag-for-method.php | 52 ++ .../material/profilereview/nag-for-mfa.php | 63 ++ .../themes/material/profilereview/review.php | 128 +++ modules/material/www/bowser.1.9.4.min.js | 6 + modules/material/www/default-favicon.ico | Bin 0 -> 5430 bytes modules/material/www/default-favicon.png | Bin 0 -> 1527 bytes modules/material/www/default-logo.png | Bin 0 -> 16314 bytes modules/material/www/email.svg | 1 + modules/material/www/material-icons.woff | Bin 0 -> 61928 bytes modules/material/www/material-icons.woff2 | Bin 0 -> 46740 bytes modules/material/www/material.1.2.1.min.js | 9 + .../www/material.blue_grey-teal.1.2.1.min.css | 8 + .../www/material.brown-orange.1.2.1.min.css | 8 + .../www/material.indigo-purple.1.2.1.min.css | 8 + .../material.orange-light_blue.1.2.1.min.css | 8 + .../www/material.red-teal.1.2.1.min.css | 8 + .../www/material.teal-blue.1.2.1.min.css | 8 + modules/material/www/mfa-backupcode.svg | 1 + modules/material/www/mfa-manager.svg | 3 + modules/material/www/mfa-totp.svg | 7 + modules/material/www/mfa-u2f-api.js | 748 ++++++++++++++++++ modules/material/www/mfa-u2f.svg | 27 + modules/material/www/mfa-webauthn.svg | 27 + modules/material/www/shield.svg | 3 + .../material/www/simplewebauthn/LICENSE.md | 21 + .../material/www/simplewebauthn/browser.js | 2 + modules/material/www/styles.2.3.6.css | 282 +++++++ 59 files changed, 3993 insertions(+) create mode 100644 docs/material_tests.md create mode 100644 modules/material/dictionaries/about2expire.definition.json create mode 100644 modules/material/dictionaries/error.definition.json create mode 100644 modules/material/dictionaries/expired.definition.json create mode 100644 modules/material/dictionaries/footer.definition.json create mode 100644 modules/material/dictionaries/login.definition.json create mode 100644 modules/material/dictionaries/logout.definition.json create mode 100644 modules/material/dictionaries/mfa.definition.json create mode 100644 modules/material/dictionaries/nag.definition.json create mode 100644 modules/material/dictionaries/review.definition.json create mode 100644 modules/material/dictionaries/selectidp.definition.json create mode 100644 modules/material/themes/material/common-announcement.php create mode 100644 modules/material/themes/material/common-footer.php create mode 100644 modules/material/themes/material/common-head-elements.php create mode 100644 modules/material/themes/material/core/loginuserpass.php create mode 100644 modules/material/themes/material/default/error.php create mode 100644 modules/material/themes/material/default/logout.php create mode 100644 modules/material/themes/material/default/selectidp-links.php create mode 100644 modules/material/themes/material/expirychecker/about2expire.php create mode 100644 modules/material/themes/material/expirychecker/expired.php create mode 100644 modules/material/themes/material/mfa/low-on-backup-codes.php create mode 100644 modules/material/themes/material/mfa/must-set-up-mfa.php create mode 100644 modules/material/themes/material/mfa/new-backup-codes.php create mode 100644 modules/material/themes/material/mfa/other_mfas.php create mode 100644 modules/material/themes/material/mfa/out-of-backup-codes.php create mode 100644 modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php create mode 100644 modules/material/themes/material/mfa/prompt-for-mfa-manager.php create mode 100644 modules/material/themes/material/mfa/prompt-for-mfa-totp.php create mode 100644 modules/material/themes/material/mfa/prompt-for-mfa-u2f.php create mode 100644 modules/material/themes/material/mfa/prompt-for-mfa-webauthn.php create mode 100644 modules/material/themes/material/mfa/send-manager-mfa.php create mode 100644 modules/material/themes/material/profilereview/nag-for-method.php create mode 100644 modules/material/themes/material/profilereview/nag-for-mfa.php create mode 100644 modules/material/themes/material/profilereview/review.php create mode 100644 modules/material/www/bowser.1.9.4.min.js create mode 100644 modules/material/www/default-favicon.ico create mode 100644 modules/material/www/default-favicon.png create mode 100644 modules/material/www/default-logo.png create mode 100644 modules/material/www/email.svg create mode 100644 modules/material/www/material-icons.woff create mode 100644 modules/material/www/material-icons.woff2 create mode 100644 modules/material/www/material.1.2.1.min.js create mode 100644 modules/material/www/material.blue_grey-teal.1.2.1.min.css create mode 100644 modules/material/www/material.brown-orange.1.2.1.min.css create mode 100644 modules/material/www/material.indigo-purple.1.2.1.min.css create mode 100644 modules/material/www/material.orange-light_blue.1.2.1.min.css create mode 100644 modules/material/www/material.red-teal.1.2.1.min.css create mode 100644 modules/material/www/material.teal-blue.1.2.1.min.css create mode 100644 modules/material/www/mfa-backupcode.svg create mode 100644 modules/material/www/mfa-manager.svg create mode 100644 modules/material/www/mfa-totp.svg create mode 100644 modules/material/www/mfa-u2f-api.js create mode 100644 modules/material/www/mfa-u2f.svg create mode 100644 modules/material/www/mfa-webauthn.svg create mode 100644 modules/material/www/shield.svg create mode 100644 modules/material/www/simplewebauthn/LICENSE.md create mode 100644 modules/material/www/simplewebauthn/browser.js create mode 100644 modules/material/www/styles.2.3.6.css diff --git a/README.md b/README.md index f920c616..c94666ca 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,75 @@ This is adapted from the `ssp-iidp-expirycheck` and `expirycheck` modules. Thanks to Alex Mihičinac, Steve Moitozo, and Steve Bagwell for the initial work they did on those two modules. +### Material Module + +Material Design theme for use with SimpleSAMLphp + +#### Installation + +``` +composer.phar require silinternational/simplesamlphp-module-material:dev-master +``` + +#### Configuration + +Update `/simplesamlphp/config/config.php`: + +``` +'theme.use' => 'material:material' +``` + +This project provides a convenience by loading this config with whatever is in the environment variable `THEME_USE`._ + +##### Google reCAPTCHA + +If a site key has been provided in `$this->data['recaptcha.siteKey']`, the +username/password page may require the user prove his/her humanity. + +##### Branding + +Update `/simplesamlphp/config/config.php`: + +``` +'theme.color-scheme' => ['indigo-purple'|'blue_grey-teal'|'red-teal'|'orange-light_blue'|'brown-orange'|'teal-blue'] +``` + +The login page looks for `/simplesamlphp/www/logo.png` which is **NOT** provided by default. + +##### Analytics + +Update `/simplesamlphp/config/config.php`: + +``` +'analytics.trackingId' => 'UA-some-unique-id-for-your-site' +``` + +This project provides a convenience by loading this config with whatever is in the environment variable `ANALYTICS_ID`._ + +##### Announcements + +Update `/simplesamlphp/announcement/announcement.php`: + +``` + return 'Some important announcement'; +``` + +By default, the announcement is whatever is returned by `/simplesamlphp/announcement/announcement.php`._ + +If provided, an alert will be shown to the user filled with the content of that announcement. HTML is supported. + +#### Testing the Material theme + +[Manual tests](./docs/material_tests.md) + +#### i18n support + +Translations are categorized by page in definition files located in the `dictionaries` directory. + +Localization is affected by the configuration setting `language.available`. Only language codes found in this property will be utilized. +For example, if a translation is provided in Afrikaans for this module, the configuration must be adjusted to make 'af' an available +language. If that's not done, the translation function will not utilize the translations even if provided. + ### Multi-Factor Authentication (MFA) simpleSAMLphp Module A simpleSAMLphp module for prompting the user for MFA credentials (such as a TOTP code, etc.). diff --git a/docs/material_tests.md b/docs/material_tests.md new file mode 100644 index 00000000..fef075af --- /dev/null +++ b/docs/material_tests.md @@ -0,0 +1,187 @@ + +# Testing the Material Module theme + +[Make](https://www.gnu.org/software/make/), [Docker](https://www.docker.com/products/overview) and +[Docker Compose](https://docs.docker.com/compose/install/) are required. + +## Setup + +1. Setup `localhost` (or `192.168.62.54`, if using Vagrant) aliases for `ssp-hub1.local`, `ssp-hub2.local`, `ssp-idp1.local`, `ssp-idp2.local`, `ssp-idp3.local`, `ssp-idp4.local`, `ssp-sp1.local` and `ssp-sp2.local`. This is typically done in `/etc/hosts`. _Example line: `0.0.0.0 ssp-hub1.local ssp-idp1.local ssp-idp2.local ssp-idp4.local ssp-hub2.local ssp-idp3.local ssp-sp1.local ssp-sp2.local`_ +1. Start test environment, i.e., `make` from the command line. + +## Hub page + +1. Goto [Hub 1](http://ssp-hub1.local/module.php/core/authenticate.php?as=hub-discovery) + +## Error page + +1. Goto [Hub 1](http://ssp-hub1.local) +1. Click **Federation** tab +1. Click either **Show metadata** link +1. Login as hub administrator: `username=`**admin** `password=`**abc123** + +## Logout page + +1. Goto [Hub 1](http://ssp-hub1.local) +1. Click **Authentication** tab +1. Click **Test configured authentication sources** +1. Click **admin** +1. Login as hub administrator: `username=`**admin** `password=`**abc123** +1. Click **Logout** + +## Login page + +### Without theme in place + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp1** (first one) +1. login page should **NOT** have material design + +### With theme in place + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp2** (second one) +1. login page **SHOULD** have material design + +## Forgot password functionality + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp2** (second one) +1. Forgot password link should be visible + +## Helpful links functionality + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Help link should be visible under login form +1. Profile link should be visible under login form + +## Expiry functionality + +### About to expire page + +_Note: This nag only works once since choosing later will simply set the nag date into the future a little._ + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp2** (second one) +1. Login as an "about to expire" user: `username=`**near_future** `password=`**a** +1. Click **Later** +1. Click **Logout** + +### Expired page + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp2** (second one) +1. Login as an "expired" user: `username=`**already_past** `password=`**a** + +## Multi-factor authentication (MFA) functionality + +### Nag about missing MFA setup + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as an "unprotected" user: `username=`**nag_for_mfa** `password=`**a** +1. The "learn more" link should be visible +1. Click **Enable** +1. Click your browser's back button +1. Click **Remind me later** +1. Click **Logout** + +### Nag about missing password recovery methods + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a user without any methods: `username=`**nag_for_method** `password=`**a** +1. Enter one of the following codes to verify (`94923279, 82743523, 77802769, 01970541, 37771076`) +1. Click **Add** +1. Click your browser's back button +1. Click **Remind me later** +1. Click **Logout** + +### Force MFA setup + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as an "unsafe" user: `username=`**must_set_up_mfa** `password=`**a** + +### Backup code + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "backup code" user: `username=`**has_backupcode** `password=`**a** +1. Enter one of the following codes to verify (`94923279, 82743523, 77802769, 01970541, 37771076`) +1. Click **Logout** +1. In order to see the "running low on codes" page, simply log back in and use another code. +1. In order to see the "out of codes" page, simply log back in and out repeatedly until there are no more codes. + +### TOTP code + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "totp" user: `username=`**has_totp** `password=`**a** +1. Set up an app using this secret, `JVRXKYTMPBEVKXLS` +1. Enter code from app to verify +1. Click **Logout** + +### Key (U2F) + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "u2f" user: `username=`**has_u2f** `password=`**a** +1. Insert key and press +1. Click **Logout** + +### Key (WebAuthn) + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "webauthn" user: `username=`**has_webauthn** `password=`**a** +1. Insert key and press +1. Click **Logout** + +### Multiple options + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "multiple option" user: `username=`**has_all** `password=`**a** +1. Click **MORE OPTIONS** + +### Multiple options (legacy, with U2F) + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "multiple option" user: `username=`**has_all_legacy** `password=`**a** +1. Click **MORE OPTIONS** + +### Manager rescue + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "multiple option" user: `username=`**has_all** `password=`**a** +1. Click **MORE OPTIONS** +1. Click the help option +1. Choose **Send** + +_NOTE: At this time, the correct code is not known and can't be tested locally (it's only available in an email to the manager)_ + +## Announcements functionality + +1. Goto [SP 2](http://ssp-sp2.local:8083/module.php/core/authenticate.php?as=hub-discovery) +1. The announcement should be displayed on the hub +1. Click **idp3** (first one) +1. The announcement should be displayed at the login screen + +## SP name functionality + +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. The sp name should appear in the banner + +## Profile review functionality +1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Click **idp4** (third one) +1. Login as a "Review needed" user: `username=`**needs_review** `password=`**a** +1. Enter one of the following printable codes to verify (`94923279, 82743523, 77802769, 01970541, 37771076`) +1. Click the button to update the profile +1. Click the button to continue +1. Click **Logout** + diff --git a/modules/material/dictionaries/about2expire.definition.json b/modules/material/dictionaries/about2expire.definition.json new file mode 100644 index 00000000..b39acd50 --- /dev/null +++ b/modules/material/dictionaries/about2expire.definition.json @@ -0,0 +1,45 @@ + +{ + "title": { + "en": "Expiring password", + "es": "Contraseña vencida", + "fr": "Mot de passe expiré", + "ko": "만료 된 암호" + }, + "header": { + "en": "Password expiring soon", + "es": "Contraseña caducada pronto", + "fr": "Mot de passe expire bientôt", + "ko": "곧 만료되는 암호" + }, + "expiring_in_a_day": { + "en": "Your password will expire in one day.", + "es": "Su contraseña caducará en un día.", + "fr": "Votre mot de passe expirera en un jour.", + "ko": "암호는 하루 만료됩니다." + }, + "expiring_soon": { + "en": "Your password will expire in {daysLeft} days.", + "es": "Su contraseña caducará en {daysLeft} días.", + "fr": "Votre mot de passe expirera en {daysLeft} jours.", + "ko": "비밀번호는 {daysLeft} 일 후에 만료됩니다." + }, + "change_now": { + "en": "Would you like to change it now?", + "es": "¿Quieres cambiarlo ahora?", + "fr": "Voulez-vous le changer maintenant?", + "ko": "지금 변경 하시겠습니까?" + }, + "button_change": { + "en": "Yes", + "es": "Sí", + "fr": "Oui", + "ko": "예" + }, + "button_continue": { + "en": "Later", + "es": "Después", + "fr": "Plus tard", + "ko": "후에" + } +} diff --git a/modules/material/dictionaries/error.definition.json b/modules/material/dictionaries/error.definition.json new file mode 100644 index 00000000..1cff9bba --- /dev/null +++ b/modules/material/dictionaries/error.definition.json @@ -0,0 +1,21 @@ + +{ + "title": { + "en": "Error", + "es": "Error", + "fr": "Erreur", + "ko": "오류" + }, + "header": { + "en": "Error", + "es": "Error", + "fr": "Erreur", + "ko": "오류" + }, + "message": { + "en": "An error occurred, please contact your help desk for further assistance.", + "es": "Se ha producido un error, póngase en contacto con su asistencia técnica para obtener más ayuda.", + "fr": "Une erreur s'est produite, s'il vous plaît contacter votre service d'assistance pour plus d'assistance.", + "ko": "오류가 발생했습니다. 도움을 받으려면 헬프 데스크에 문의하십시오." + } +} diff --git a/modules/material/dictionaries/expired.definition.json b/modules/material/dictionaries/expired.definition.json new file mode 100644 index 00000000..0915a459 --- /dev/null +++ b/modules/material/dictionaries/expired.definition.json @@ -0,0 +1,27 @@ + +{ + "title": { + "en": "Expired password", + "es": "Contraseña caducada", + "fr": "Mot de passe expiré", + "ko": "만료 된 암호" + }, + "header": { + "en": "Password expired", + "es": "La contraseña expiró", + "fr": "Mot de passe expiré", + "ko": "암호가 만료되었습니다." + }, + "expired": { + "en": "Your password has expired and must be changed before continuing.", + "es": "Su contraseña ha caducado y debe cambiarse antes de continuar.", + "fr": "Votre mot de passe a expiré et doit être modifié avant de continuer.", + "ko": "비밀번호가 만료되었으므로 계속하기 전에 비밀번호를 변경해야합니다." + }, + "button_change": { + "en": "Change", + "es": "Cambiar", + "fr": "Changer", + "ko": "바꾸다" + } +} diff --git a/modules/material/dictionaries/footer.definition.json b/modules/material/dictionaries/footer.definition.json new file mode 100644 index 00000000..0ee65023 --- /dev/null +++ b/modules/material/dictionaries/footer.definition.json @@ -0,0 +1,9 @@ + +{ + "copyright": { + "en": "Unauthorized use of this site is prohibited and may be subjected to civil and criminal prosecution.", + "es": "El uso no autorizado de este sitio está prohibido y puede ser sometido a procesamiento civil y penal.", + "fr": "L'utilisation non autorisée de ce site est interdite et peut faire l'objet de poursuites civiles et pénales.", + "ko": "이 사이트의 무단 사용은 금지되어 있으며 민사 및 형사 고발의 대상이 될 수 있습니다." + } +} diff --git a/modules/material/dictionaries/login.definition.json b/modules/material/dictionaries/login.definition.json new file mode 100644 index 00000000..e5d1d69b --- /dev/null +++ b/modules/material/dictionaries/login.definition.json @@ -0,0 +1,62 @@ +{ + "title": { + "en": "Login with your {idpName} identity", + "es": "Inicie sesión con su identidad de {idpName}", + "fr": "Connectez-vous avec votre identité {idpName}", + "ko": "{idpName} 신원 계정으로 로그인하십시오." + }, + "header": { + "en": "Login with your {idpName} identity", + "es": "Inicie sesión con su identidad de {idpName}", + "fr": "Connectez-vous avec votre identité {idpName}", + "ko": "{idpName} 신원 계정으로 로그인하십시오." + }, + "label_username": { + "en": "Username", + "es": "Nombre de usuario", + "fr": "Nom d'utilisateur", + "ko": "사용자 이름" + }, + "label_password": { + "en": "Password", + "es": "Contraseña", + "fr": "Mot de passe", + "ko": "암호" + }, + "error_wronguserpass": { + "en": "Something is wrong with that username or password, please verify and try again.", + "es": "Algo está mal con ese nombre de usuario o contraseña, compruebe e inténtelo de nuevo.", + "fr": "Quelque chose ne va pas avec ce nom d'utilisateur ou ce mot de passe, veuillez vérifier et essayer à nouveau.", + "ko": "해당 사용자 이름 또는 비밀번호가 잘못되었습니다. 다시 확인하고 다시 시도하십시오." + }, + "button_login": { + "en": "Login", + "es": "Iniciar sesión", + "fr": "Connexion", + "ko": "로그인" + }, + "forgot": { + "en": "Forgot password?", + "es": "¿Se te olvidó tu contraseña?", + "fr": "Mot de passe oublié?", + "ko": "비밀번호를 잊으 셨나요?" + }, + "logo": { + "en": "{idpName} logo", + "es": "Logotipo de {idpName}", + "fr": "Logo {idpName}", + "ko": "{idpName} 로고" + }, + "help": { + "en": "I need help", + "es": "necesito ayuda", + "fr": "j'ai besoin d'aide", + "ko": "도움이 필요해." + }, + "profile": { + "en": "Manage my profile", + "es": "Administrar mi perfil", + "fr": "Gérer mon profil", + "ko": "내 프로필 관리" + } +} diff --git a/modules/material/dictionaries/logout.definition.json b/modules/material/dictionaries/logout.definition.json new file mode 100644 index 00000000..61324e70 --- /dev/null +++ b/modules/material/dictionaries/logout.definition.json @@ -0,0 +1,21 @@ + +{ + "title": { + "en": "Logged out", + "es": "Desconectado", + "fr": "Déconnecté", + "ko": "로그 아웃 됨" + }, + "header": { + "en": "Logged out", + "es": "Desconectado", + "fr": "Déconnecté", + "ko": "로그 아웃 됨" + }, + "message": { + "en": "You have now been logged out.", + "es": "Se ha desconectado.", + "fr": "Vous êtes maintenant déconnecté.", + "ko": "이제 로그 아웃되었습니다." + } +} diff --git a/modules/material/dictionaries/mfa.definition.json b/modules/material/dictionaries/mfa.definition.json new file mode 100644 index 00000000..1d992b32 --- /dev/null +++ b/modules/material/dictionaries/mfa.definition.json @@ -0,0 +1,399 @@ + +{ + "title": { + "en": "2-Step Verification", + "es": "Verificación en 2 pasos", + "fr": "Vérification en deux étapes", + "ko": "2 단계 인증" + }, + "header": { + "en": "2-Step Verification", + "es": "Verificación en 2 pasos", + "fr": "Vérification en deux étapes", + "ko": "2 단계 인증" + }, + "backupcode_header": { + "en": "Printable code", + "es": "código imprimible", + "fr": "code imprimable", + "ko": "인쇄 가능한 코드" + }, + "backupcode_icon": { + "en": "Printable code icon", + "es": "icono de código imprimible", + "fr": "icône de code imprimable", + "ko": "인쇄 가능한 코드 아이콘" + }, + "backupcode_reminder": { + "en": "Each code can only be used once, so the code you enter this time will be used up and will not be available again.", + "es": "Cada código solo se puede usar una vez, por lo que el código que ingrese esta vez se agotará y no estará disponible nuevamente.", + "fr": "Chaque code ne peut être utilisé qu'une seule fois, de sorte que le code que vous entrez cette fois sera épuisé et ne sera plus disponible.", + "ko": "각 코드는 한번만 사용할 수 있으므로 이번에 입력한 코드는 소멸되어 다시 사용할 수 없습니다." + }, + "backupcode_input": { + "en": "Enter code", + "es": "Introduzca el código", + "fr": "Entrer le code", + "ko": "코드 입력" + }, + "totp_header": { + "en": "Smartphone app", + "es": "Aplicación de teléfono inteligente", + "fr": "Application pour smartphone", + "ko": "스마트폰 앱" + }, + "totp_icon": { + "en": "Smartphone app icon", + "es": "Icono de aplicación de teléfono inteligente", + "fr": "Icône de l'application Smartphone", + "ko": "스마트폰 응용 프로그램 아이콘" + }, + "totp_instructions": { + "en": "You will need to check your smartphone app for the current code.", + "es": "Deberá verificar la aplicación de su teléfono inteligente para ver el código actual.", + "fr": "Vous devriez vérifier l'application sur votre smartphone pour voir le code actuel.", + "ko": "스마트폰 앱에서 현재 코드를 확인해야합니다." + }, + "totp_input": { + "en": "Enter 6-digit code", + "es": "Ingrese el código de 6 dígitos", + "fr": "Entrer le code à 6 chiffres", + "ko": "6 자리 코드 입력" + }, + "u2f_header": { + "en": "Security key", + "es": "Clave de seguridad", + "fr": "Clé de sécurité", + "ko": "보안키" + }, + "webauthn_header": { + "en": "Security key", + "es": "Clave de seguridad", + "fr": "Clé de sécurité", + "ko": "보안키" + }, + "u2f_icon": { + "en": "USB key icon", + "es": "Icono de la llave USB", + "fr": "Icône de clé USB", + "ko": "USB 키 아이콘" + }, + "webauthn_icon": { + "en": "USB key icon", + "es": "Icono de la llave USB", + "fr": "Icône de clé USB", + "ko": "USB 키 아이콘" + }, + "u2f_instructions": { + "en": "You may now insert your security key and press its button.", + "es": "Ahora puede insertar su clave de seguridad y presionar su botón.", + "fr": "Vous pouvez maintenant insérer votre clé de sécurité et appuyer sur le bouton.", + "ko": "이제 보안 키를 삽입하고 단추를 누를 수 있습니다." + }, + "webauthn_instructions": { + "en": "You may now insert your security key and press its button.", + "es": "Ahora puede insertar su clave de seguridad y presionar su botón.", + "fr": "Vous pouvez maintenant insérer votre clé de sécurité et appuyer sur le bouton.", + "ko": "이제 보안 키를 삽입하고 단추를 누를 수 있습니다." + }, + "u2f_unsupported": { + "en": "Unsupported in your current browser. Please consider a more secure browser like Google Chrome.", + "es": "No compatible en su navegador actual. Considere un navegador más seguro como Google Chrome.", + "fr": "Non compatible avec votre navigateur actuel. Veuillez considérer un navigateur plus sûr comme Google Chrome.", + "ko": "현재 브라우저에서 지원되지 않습니다. Chrome과 같은 보다 안전한 브라우저를 고려하십시오." + }, + "webauthn_unsupported": { + "en": "Unsupported in your current browser. Please consider a more secure browser like Google Chrome.", + "es": "No compatible en su navegador actual. Considere un navegador más seguro como Google Chrome.", + "fr": "Non compatible avec votre navigateur actuel. Veuillez considérer un navigateur plus sûr comme Google Chrome.", + "ko": "현재 브라우저에서 지원되지 않습니다. Chrome과 같은 보다 안전한 브라우저를 고려하십시오." + }, + "u2f_error_unknown": { + "en": "Something went wrong with that request, unable to verify at this time.", + "es": "Algo salió mal con esa solicitud, no se pudo verificar en este momento.", + "fr": "Quelque chose s'est mal passé avec cette demande, impossible de vérifier pour le moment.", + "ko": "요청에 문제가 발생하여 지금은 확인할 수 없습니다." + }, + "webauthn_error_unknown": { + "en": "Something went wrong with that request, unable to verify at this time.", + "es": "Algo salió mal con esa solicitud, no se pudo verificar en este momento.", + "fr": "Quelque chose s'est mal passé avec cette demande, impossible de vérifier pour le moment.", + "ko": "요청에 문제가 발생하여 지금은 확인할 수 없습니다." + }, + "u2f_error_wrong_key": { + "en": "This may not be the correct key for this site.", + "es": "Esta puede no ser la clave correcta para este sitio.", + "fr": "Ce n'est peut-être pas la bonne clé pour ce site.", + "ko": "이 사이트의 올바른 키가 아닐 수도 있습니다." + }, + "u2f_error_timeout": { + "en": "That took a little too long, check to make sure your key is inserted right-side up.", + "es": "Eso llevó demasiado tiempo, verifique que su clave esté insertada boca arriba.", + "fr": "Cela a pris un peu trop de temps, vérifiez que votre clé est insérée dans le bons sens.", + "ko": "오랜 시간이 경과 되었으니 키가 오른쪽 위로 삽입되었는지 확인 하십시오." + }, + "webauthn_error_abort": { + "en": "It looks like you clicked cancel. Would you like us to try again?", + "es": "It looks like you clicked cancel. Would you like us to try again?", + "fr": "Il semble que vous ayez cliqué sur annuler. Souhaitez-vous que nous essayions à nouveau ?", + "ko": "It looks like you clicked cancel. Would you like us to try again?" + }, + "webauthn_error_not_allowed": { + "en": "Something about that didn't work. Please ensure that your security key is plugged in and that you touch it within 60 seconds when it blinks.", + "es": "Something about that didn't work. Please ensure that your security key is plugged in and that you touch it within 60 seconds when it blinks.", + "fr": "Quelque chose n'a pas fonctionné avec ça. Veuillez vous assurer que votre clé de sécurité est insérée et que vous la touchez dans les 60 secondes lorsqu'elle clignote.", + "ko": "Something about that didn't work. Please ensure that your security key is plugged in and that you touch it within 60 seconds when it blinks." + }, + "manager_icon": { + "en": "Recovery contact icon", + "es": "Icono de contacto de recuperación", + "fr": "Icône du contact de récupération", + "ko": "복구 연락처 아이콘" + }, + "manager_header": { + "en": "Recovery contact help", + "es": "Ayuda de contacto de recuperación", + "fr": "Aide de contact de récupération", + "ko": "복구 연락처" + }, + "manager_info": { + "en": "We can send a code to your recovery contact which can be used as a temporary 2-Step Verification option. The email address on file (masked for privacy) is {managerEmail}.", + "es": "Podemos enviar un código a su contacto de recuperación que puede usarse como una opción de Verificación temporal de 2 pasos. La dirección de correo electrónico en el archivo (enmascarada por privacidad) es {managerEmail}.", + "fr": "Nous pouvons envoyer un code à votre contact de récupération, qui peut être utilisé comme option de vérification temporaire en deux étapes. L'adresse électronique au dossier (masquée pour la confidentialité) est {managerEmail}.", + "ko": "\n임시 2 단계 인증 옵션으로 사용할 수있는 코드를 복구 담당자에게 보낼 수 있습니다. 파일의 이메일 주소 (개인 정보 보호를 위해 마스크 됨)는 {managerEmail}입니다." + }, + "manager_sent": { + "en": "A temporary code was sent your recovery contact at {managerEmail}.", + "es": "Se envió un código temporal a su contacto de recuperación en {managerEmail}.", + "fr": "Un code temporaire a été envoyé à votre contact de récupération à l'adresse {managerEmail}.", + "ko": "{managerEmail} (으)로 복구 담당자에게 임시 코드를 보냈습니다." + }, + "manager_input": { + "en": "Enter code", + "es": "Introduzca el código", + "fr": "Entrer le code", + "ko": "코드 입력" + }, + "shield_icon": { + "en": "Shield icon", + "es": "Icono de escudo", + "fr": "Icône de bouclier", + "ko": "방패 아이콘" + }, + "required_header": { + "en": "Protect this account", + "es": "Protege esta cuenta", + "fr": "Protéger ce compte", + "ko": "이 계정 보호" + }, + "required_info": { + "en": "Your identity account requires additional security, you must set up 2-Step Verification at this time.", + "es": "Su cuenta de identidad requiere seguridad adicional, debe configurar la verificación en dos pasos en este momento.", + "fr": "Votre compte d'identité nécessite une sécurité supplémentaire, vous devez configurer la vérification en deux étapes en ce moment.", + "ko": "신원 계정에 추가 보안이 필요하므로 현재 2 단계 인증을 설정해야합니다." + }, + "running_out_header": { + "en": "Almost out of printable codes", + "es": "Casi sin códigos imprimibles", + "fr": "Codes imprimables presque épuisés", + "ko": "인쇄 ​가능한 ​코드​가 거의 남지 않았습니다" + }, + "running_out_info": { + "en": "You only have {numBackupCodesRemaining} more left.", + "es": "Solo tiene {numBackupCodesRemaining} más disponible.", + "fr": "Vous avez seulement {numBackupCodesRemaining} qui restent.", + "ko": "{numBackupCodesRemaining} 만 남았습니다." + }, + "no_more_codes_header": { + "en": "Last printable code used", + "es": "Último código imprimible utilizado", + "fr": "Dernier code imprimable utilisé", + "ko": "​​마지막 인쇄 가능 코드가​​ ​사용되었습니다." + }, + "new_codes_header": { + "en": "New printable codes", + "es": "Nuevos códigos imprimibles", + "fr": "Nouveaux codes imprimables", + "ko": "​​새로운 인쇄 가능 코드" + }, + "old_codes_gone": { + "en": "You may now discard any of your previous codes, they have been deleted.", + "es": "Ahora puede descartar cualquiera de sus códigos anteriores, se han eliminado.", + "fr": "Vous pouvez maintenant jeter tous vos codes précédents, ils ont été supprimés.", + "ko": "​​이제 이전 코드를 삭제해도 삭제 될 수 있습니다." + }, + "new_codes_info": { + "en": "Printable codes should be treated with the same level of attention as any password.", + "es": "Los códigos imprimibles deben tratarse con el mismo nivel de atención que cualquier contraseña.", + "fr": "Les codes imprimables doivent être traités avec le même niveau d'attention que les mots de passe.", + "ko": "​​인쇄 가능한 코드는 모든 비밀번호와 동일한주의 수준으로 처리되어야합니다." + }, + "new_codes_only_once": { + "en": "Each code may only be used once.", + "es": "Cada código solo puede usarse una vez.", + "fr": "Chaque code ne peut être utilisé qu'une seule fois.", + "ko": "​​각 코드는 한 번만 사용할 수 있습니다." + }, + "new_codes_failed": { + "en": "Something went wrong while creating new printable codes for you. We are sorry for the inconvenience, please check your configuration at the following address after continuing: ", + "es": "Algo salió mal al crear nuevos códigos imprimibles para usted. Disculpe las molestias, compruebe su configuración en la siguiente dirección después de continuar: ", + "fr": "Une erreur s'est produite lors de la création de nouveaux codes imprimables. Nous sommes désolés pour le dérangement. Veuillez vérifier votre configuration à l'adresse suivante après avoir continué: ", + "ko": "새로운 인쇄 가능한 코드를 만드는 동안 문제가 발생했습니다. 불편을 끼쳐 드려 죄송합니다. 계속 진행 한 후 다음 주소로 구성을 확인하십시오. " + }, + "new_codes_saved": { + "en": "I saved a personal copy of these for later use", + "es": "Guardé una copia personal de estos para uso posterior", + "fr": "J'ai sauvegardé une copie personnelle de ceux-ci pour une utilisation ultérieure", + "ko": "나는 나중에 사용하기 위해 이들의 개인 사본을 저장했다." + }, + "account": { + "en": "{idpName} identity account", + "es": "Cuenta de identidad de {idpName}", + "fr": "Compte d'identité {idpName}", + "ko": "{idpName} 신원 계정" + }, + "has_options_besides_codes": { + "en": "Thankfully you do have other 2-Step Verification options set up but you should create more Printable codes if you plan to need them in the future.", + "es": "Afortunadamente, tiene otras opciones de verificación en dos pasos configuradas, pero debe crear más códigos imprimibles si planea necesitarlos en el futuro.", + "fr": "Heureusement, vous avez d'autres options de vérification en deux étapes, mais vous devriez créer plus de codes imprimables si vous prévoyez en avoir besoin à l'avenir.", + "ko": "다른 2 단계 인증 옵션​은​ 설정​ 되었으나​ ​인쇄 가능 코드가 나중에 ​필요할 ​것으로 ​예상되면​ 코드를 ​더 ​만들어야​ ​합니다." + }, + "has_no_more_options": { + "en": "Since you do not have any other 2-Step Verification options set up at this time, you will need to get more Printable codes before another one is required.", + "es": "Como no tiene configuradas otras opciones de verificación en dos pasos en este momento, necesitará obtener más códigos imprimibles antes de que se requiera otro.", + "fr": "Comme aucune autre option de vérification en deux étapes n'est configurée pour l'instant, vous devez obtenir davantage de codes imprimables avant d'en avoir besoin d'un autre.", + "ko": "코드​가 요구되기 전에 인쇄 가능 코드를​ 더 가져와야​ ​합니다." + }, + "use_others": { + "en": "More options", + "es": "Mas opciones", + "fr": "Davantage d'options", + "ko": "추가 옵션" + }, + "use_u2f": { + "en": "Use my security key instead", + "es": "Use mi clave de seguridad en su lugar", + "fr": "Utiliser plutôt ma clé de sécurité", + "ko": "내 보안키 사용" + }, + "use_webauthn": { + "en": "Use my security key instead", + "es": "Use mi clave de seguridad en su lugar", + "fr": "Utiliser plutôt ma clé de sécurité", + "ko": "내 보안키 사용" + }, + "use_totp": { + "en": "Use my smartphone app instead", + "es": "Use la aplicación de mi teléfono inteligente en su lugar", + "fr": "Utiliser plutôt mon application smartphone", + "ko": "내 스마트폰 앱 사용" + }, + "use_backupcode": { + "en": "Use a printable code instead", + "es": "Use un código imprimible en su lugar", + "fr": "Utiliser plutôt un code imprimable", + "ko": "인쇄 가능한 코드 사용" + }, + "use_help": { + "en": "I need help", + "es": "necesito ayuda", + "fr": "j'ai besoin d'aide", + "ko": "도움이 필요해." + }, + "use_manager": { + "en": "Use code from my recovery contact", + "es": "Usar código de mi contacto de recuperación", + "fr": "Utiliser le code de mon contact de récupération", + "ko": "복구 담당자의 코드 사용" + }, + "button_verify": { + "en": "Verify", + "es": "Verificar", + "fr": "Vérifier", + "ko": "검증" + }, + "button_later": { + "en": "Remind me later", + "es": "Recuérdame más tarde", + "fr": "Rappelez-moi plus tard", + "ko": "추후 알림" + }, + "button_enable": { + "en": "Enable now", + "es": "Habilite ahora", + "fr": "Activer maintenant", + "ko": "지금 사용" + }, + "button_set_up": { + "en": "Set up now", + "es": "Configurar ahora", + "fr": "Configurer maintenant", + "ko": "지금 설정" + }, + "button_try_again": { + "en": "Try again", + "es": "Inténtalo de nuevo", + "fr": "Essayer de nouveau", + "ko": "다시 시도" + }, + "button_get_more": { + "en": "Get more", + "es": "Obtenga más", + "fr": "Avoir plus", + "ko": "더​ ​​가져오기" + }, + "button_continue": { + "en": "Continue", + "es": "Continuar", + "fr": "Continuer", + "ko": "계속하다" + }, + "button_print": { + "en": "Print", + "es": "Imprimir", + "fr": "Imprimer", + "ko": "인쇄" + }, + "button_download": { + "en": "Download", + "es": "Descargar", + "fr": "Télécharger", + "ko": "다운로드" + }, + "button_copy": { + "en": "Copy", + "es": "Copiar", + "fr": "Copier", + "ko": "사본" + }, + "button_send": { + "en": "Send", + "es": "Enviar", + "fr": "Envoyer", + "ko": "보내다" + }, + "button_cancel": { + "en": "Cancel", + "es": "Cancelar", + "fr": "Annuler", + "ko": "취소" + }, + "button_copied": { + "en": "Copied ✓", + "es": "Copiado ✓", + "fr": "Copié ✓", + "ko": "복사 됨 ✓" + }, + "remember_this": { + "en": "Remember this browser for 30 days", + "es": "Recuerde esta navegador por 30 días", + "fr": "Se rappeler de ce navigatuer pour 30 jours", + "ko": "이 브라우저를 30일간 기억" + }, + "unsupported": { + "en": "Not supported in this browser", + "es": "No compatible con este navegador", + "fr": "Non pris en charge dans ce navigateur", + "ko": "이 브라우저에서는 지원되지 않습니다." + } +} diff --git a/modules/material/dictionaries/nag.definition.json b/modules/material/dictionaries/nag.definition.json new file mode 100644 index 00000000..9c450e90 --- /dev/null +++ b/modules/material/dictionaries/nag.definition.json @@ -0,0 +1,75 @@ + +{ + "mfa_title": { + "en": "2-Step Verification", + "es": "Verificación en 2 pasos", + "fr": "Vérification en deux étapes", + "ko": "2 단계 인증" + }, + "mfa_header": { + "en": "2-Step Verification", + "es": "Verificación en 2 pasos", + "fr": "Vérification en deux étapes", + "ko": "2 단계 인증" + }, + "method_title": { + "en": "Password recovery methods", + "es": "Métodos de recuperación de contraseña", + "fr": "Méthodes de récupération de mot de passe", + "ko": "비밀번호 복구 방법" + }, + "method_header": { + "en": "Password recovery methods", + "es": "Métodos de recuperación de contraseña", + "fr": "Méthodes de récupération de mot de passe", + "ko": "비밀번호 복구 방법" + }, + "shield_icon": { + "en": "Shield icon", + "es": "Icono de escudo", + "fr": "Icône de bouclier", + "ko": "방패 아이콘" + }, + "header": { + "en": "Protect yourself", + "es": "Protéjase", + "fr": "Protégez-vous", + "ko": "자기 보호" + }, + "mfa_info": { + "en": "Did you know you could easily increase the security of your identity account by enabling 2-Step Verification?", + "es": "¿Sabía que podría aumentar fácilmente la seguridad de su cuenta de identidad al habilitar la verificación en dos pasos?", + "fr": "Savez-vous que vous pouvez facilement augmenter la sécurité de votre compte d'identité en activant la vérification en deux étapes?", + "ko": "2 단계 인증을 사용하여 신원 계정의 보안을 쉽게 높일 수 있다는 사실을 알고 계셨습니까?" + }, + "method_info": { + "en": "Do you forget your password sometimes? Did you know it is very easy to add an alternate email address for password recovery just in case?", + "es": "¿Olvidas tu contraseña a veces? ¿Sabía que es muy fácil agregar una dirección de correo electrónico alternativa para recuperar la contraseña por si acaso?", + "fr": "Avez-vous oublié votre mot de passe parfois? Saviez-vous qu'il est très facile d'ajouter une adresse électronique de remplacement pour la récupération du mot de passe au cas où?", + "ko": "가끔 암호를 잊어 버리십니까? 혹시라도 비밀번호 복구를 위해 보조 이메일 주소를 추가하는 것이 매우 쉽다는 것을 알고 계셨습니까?" + }, + "button_later": { + "en": "Remind me later", + "es": "Recuérdame más tarde", + "fr": "Rappelez-moi plus tard", + "ko": "추후 알림" + }, + "button_learn_more": { + "en": "Learn more", + "es": "Aprende más", + "fr": "Apprendre encore plus", + "ko": "더 알아보기" + }, + "button_enable": { + "en": "Enable now", + "es": "Habilite ahora", + "fr": "Activer maintenant", + "ko": "지금 사용" + }, + "button_add": { + "en": "Add one now", + "es": "Agrega uno ahora", + "fr": "Ajouter un maintenant", + "ko": "지금 하나 추가" + } +} diff --git a/modules/material/dictionaries/review.definition.json b/modules/material/dictionaries/review.definition.json new file mode 100644 index 00000000..96764c14 --- /dev/null +++ b/modules/material/dictionaries/review.definition.json @@ -0,0 +1,75 @@ + +{ + "title": { + "en": "Profile review", + "es": "Revisión del perfil", + "fr": "Examen du profil", + "ko": "프로필 검토" + }, + "header": { + "en": "Profile review", + "es": "Revisión del perfil", + "fr": "Examen du profil", + "ko": "프로필 검토" + }, + "info": { + "en": "Are these still correct?", + "es": "¿Siguen siendo correctos?", + "fr": "Sont-ils toujours corrects?", + "ko": "여전히 맞습니까?" + }, + "mfa_header": { + "en": "2-Step Verification", + "es": "Verificación en 2 pasos", + "fr": "Vérification en 2 étapes", + "ko": "2 단계 인증" + }, + "methods_header": { + "en": "Password Recovery Methods", + "es": "Métodos de recuperación de contraseña", + "fr": "Méthodes de récupération de mot de passe", + "ko": "비밀번호 복구 방법" + }, + "remaining": { + "en": "({count} remaining)", + "es": "({count} restante)", + "fr": "({count} restant)", + "ko": "({count} 남음)" + }, + "used": { + "en": "last used: {when}", + "es": "último uso: {when}", + "fr": "dernière utilisation: {when}", + "ko": "마지막 사용 시간 : {when}" + }, + "used_never": { + "en": "last used: Never", + "es": "último uso: nunca", + "fr": "Dernière utilisation: Jamais", + "ko": "마지막 사용 : Never" + }, + "verified": { + "en": "Verified", + "es": "Verificado", + "fr": "Vérifié", + "ko": "검증 된" + }, + "unverified": { + "en": "Unverified", + "es": "Inconfirmado", + "fr": "Non vérifié", + "ko": "확인되지 않음" + }, + "button_update": { + "en": "Some of these need updating", + "es": "Algunos de estos necesitan actualización", + "fr": "Certains ont besoin d'être mis à jour", + "ko": "이들 중 일부는 업데이트해야합니다." + }, + "button_continue": { + "en": "These are still correct", + "es": "Estos siguen siendo correctos", + "fr": "Ceux-ci sont toujours corrects", + "ko": "이들은 여전히 정확하다." + } +} diff --git a/modules/material/dictionaries/selectidp.definition.json b/modules/material/dictionaries/selectidp.definition.json new file mode 100644 index 00000000..55a261ac --- /dev/null +++ b/modules/material/dictionaries/selectidp.definition.json @@ -0,0 +1,38 @@ +{ + "title": { + "en": "Choose an identity account", + "es": "Elige una cuenta de identidad", + "fr": "Choisissez un compte d'identité", + "ko": "ID 계정 선택" + }, + "header": { + "en": "Choose an identity account", + "es": "Elige una cuenta de identidad", + "fr": "Choisissez un compte d'identité", + "ko": "ID 계정 선택" + }, + "header-for-sp": { + "en": "Choose an identity account to continue to {spName}", + "es": "Elija una cuenta de identidad para continuar en {spName}", + "fr": "Choisissez un compte d'identité pour continuer vers {spName}", + "ko": "{spName}을 계속 진행하려면 신원 계정을 선택하십시오." + }, + "enabled": { + "en": "Login with your {idpName} identity account", + "es": "Inicie sesión con su cuenta de identidad {idpName}", + "fr": "Connectez-vous avec votre compte d'identité {idpName}", + "ko": "{idpName} 신원 계정으로 로그인하십시오." + }, + "disabled": { + "en": "{idpName} coming soon", + "es": "{IdpName} próximamente", + "fr": "{IdpName} à venir", + "ko": "{idpName} 곧 제공됨" + }, + "help": { + "en": "Help", + "es": "Ayuda", + "fr": "Aidez-moi", + "ko": "도움" + } +} diff --git a/modules/material/themes/material/common-announcement.php b/modules/material/themes/material/common-announcement.php new file mode 100644 index 00000000..74d5649b --- /dev/null +++ b/modules/material/themes/material/common-announcement.php @@ -0,0 +1,15 @@ + +
+ +
+ diff --git a/modules/material/themes/material/common-footer.php b/modules/material/themes/material/common-footer.php new file mode 100644 index 00000000..90fb7756 --- /dev/null +++ b/modules/material/themes/material/common-footer.php @@ -0,0 +1,3 @@ +
+ t('{material:footer:copyright}') ?> +
diff --git a/modules/material/themes/material/common-head-elements.php b/modules/material/themes/material/common-head-elements.php new file mode 100644 index 00000000..140d0899 --- /dev/null +++ b/modules/material/themes/material/common-head-elements.php @@ -0,0 +1,39 @@ + + + + + + + +configuration->getValue('analytics.trackingId')); +if (! empty($trackingId)) { +?> + + + + + + + +configuration->getValue('theme.color-scheme') ?: 'indigo-purple'); +?> + + + + + + diff --git a/modules/material/themes/material/core/loginuserpass.php b/modules/material/themes/material/core/loginuserpass.php new file mode 100644 index 00000000..bc4a62af --- /dev/null +++ b/modules/material/themes/material/core/loginuserpass.php @@ -0,0 +1,159 @@ + + + + configuration->getValue( + 'idp_display_name', + $this->configuration->getValue('idp_name', '—') + )); + ?> + + <?= $this->t('{material:login:title}', ['{idpName}' => $idpName]) ?> + + + + + + data['recaptcha.siteKey'] ?? null); + + if (! empty($siteKey)) { + ?> + + + + + + +
+
+ + +
+ + + data)) { + $csrfToken = htmlentities($this->data['csrfToken']); + ?> + + + +
+
+ <?= $this->t('{material:login:logo}', ['{idpName}' => $idpName]) ?> +
+ +
+

+ t('{material:login:header}', ['{idpName}' => $idpName]) ?> +

+
+ +
+
+ + + data['username'] ?? null); + ?> + id="username"/> +
+ +
+ + + + id="password"/> +
+
+ + data['errorcode'] ?? null; + if ($errorCode == 'WRONGUSERPASS') { + $errorMessageKey = $this->data['errorparams'][1] ?? '{material:login:error_wronguserpass}'; + $errorMessageTokens = $this->data['errorparams'][2] ?? null; + + $message = $this->t($errorMessageKey, $errorMessageTokens); + ?> +

+ error + + + + +

+ + + + +
+ configuration->getValue('passwordForgotUrl')); + if (! empty($forgotPasswordUrl)) { + ?> + + t('{material:login:forgot}') ?> + + + + + + +
+
+ +
+ data['helpCenterUrl'])): ?> + + t('{material:login:help}') ?> launch + + + + data['profileUrl'])): ?> + + t('{material:login:profile}') ?> launch + + +
+
+
+
+ + diff --git a/modules/material/themes/material/default/error.php b/modules/material/themes/material/default/error.php new file mode 100644 index 00000000..859b3fbf --- /dev/null +++ b/modules/material/themes/material/default/error.php @@ -0,0 +1,41 @@ + + + + <?= $this->t('{material:error:title}') ?> + + + + +
+
+
+ + t('{material:error:header}') ?> + +
+
+ +
+

+ t('{material:error:message}') ?> +

+ + data['showerrors'] ?? false) { + ?> +

+ data['error']['exceptionMsg']) ?> +

+ +
+            data['error']['exceptionTrace']) ?>
+        
+ +
+ + +
+ + diff --git a/modules/material/themes/material/default/logout.php b/modules/material/themes/material/default/logout.php new file mode 100644 index 00000000..c1d81d34 --- /dev/null +++ b/modules/material/themes/material/default/logout.php @@ -0,0 +1,27 @@ + + + + <?= $this->t('{material:logout:title}') ?> + + + + +
+
+
+ + t('{material:logout:header}') ?> + +
+
+ +
+

+ t('{material:logout:message}') ?> +

+
+ + +
+ + diff --git a/modules/material/themes/material/default/selectidp-links.php b/modules/material/themes/material/default/selectidp-links.php new file mode 100644 index 00000000..a5d6df67 --- /dev/null +++ b/modules/material/themes/material/default/selectidp-links.php @@ -0,0 +1,124 @@ + + + + <?= $this->t('{material:selectidp:title}') ?> + + + + + + + +
+
+
+ + data['spName'] ?? null; + if (empty($spName)) { + echo $this->t('{material:selectidp:header}'); + } else { + echo htmlentities($this->t('{material:selectidp:header-for-sp}', ['{spName}' => $spName])); + } + ?> + + +
+ + data['helpCenterUrl'])): ?> + + +
+
+ +
+ + +
+ + + + + data['idplist']['dummy']); + + $enabledIdps = []; + $disabledIdps = []; + foreach ($this->data['idplist'] as $idp) { + $idp['enabled'] === true ? $enabledIdps[] = $idp + : $disabledIdps[] = $idp; + } + + foreach ($enabledIdps as $idp) { + $name = htmlentities($this->t($idp['name'])); + $idpId = htmlentities($idp['entityid']); + $hoverText = $this->t('{material:selectidp:enabled}', ['{idpName}' => $name]); + ?> +
+
+ +
+
+ + + t($idp['name'])); + $idpId = htmlentities($idp['entityid']); + $hoverText = $this->t('{material:selectidp:disabled}', ['{idpName}' => $name]); + ?> +
+
+ +
+
+ +
+
+ + + +
+ + diff --git a/modules/material/themes/material/expirychecker/about2expire.php b/modules/material/themes/material/expirychecker/about2expire.php new file mode 100644 index 00000000..59e8f0fe --- /dev/null +++ b/modules/material/themes/material/expirychecker/about2expire.php @@ -0,0 +1,58 @@ + + + + <?= $this->t('{material:about2expire:title}') ?> + + + + +
+
+
+ + t('{material:about2expire:header}') ?> + +
+
+
+
+ data['formData'] as $name => $value) { + ?> + + + +

+ data['daysLeft'] ?? 0; + $expiringMessage = $daysLeft < 2 ? + $this->t('{material:about2expire:expiring_in_a_day}') : + $this->t('{material:about2expire:expiring_soon}', + ['{daysLeft}' => $daysLeft]); + ?> + +

+ +

+ t('{material:about2expire:change_now}') ?> +

+ +
+ + + +
+
+
+ + +
+ + diff --git a/modules/material/themes/material/expirychecker/expired.php b/modules/material/themes/material/expirychecker/expired.php new file mode 100644 index 00000000..447fd685 --- /dev/null +++ b/modules/material/themes/material/expirychecker/expired.php @@ -0,0 +1,41 @@ + + + + <?= $this->t('{material:expired:title}') ?> + + + + +
+
+
+ + t('{material:expired:header}') ?> + +
+
+
+
+ data['formData'] as $name => $value) { + ?> + + + +

+ t('{material:expired:expired}') ?> +

+ + +
+
+ + +
+ + diff --git a/modules/material/themes/material/mfa/low-on-backup-codes.php b/modules/material/themes/material/mfa/low-on-backup-codes.php new file mode 100644 index 00000000..152207dd --- /dev/null +++ b/modules/material/themes/material/mfa/low-on-backup-codes.php @@ -0,0 +1,52 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+
+
+ warning +
+ +
+

+ t('{material:mfa:running_out_header}') ?> +

+
+ +
+

+ t('{material:mfa:running_out_info}', ['{numBackupCodesRemaining}' => (int)$this->data['numBackupCodesRemaining']]) ?> +

+
+ +
+ + + + + +
+
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/must-set-up-mfa.php b/modules/material/themes/material/mfa/must-set-up-mfa.php new file mode 100644 index 00000000..8ae5c34f --- /dev/null +++ b/modules/material/themes/material/mfa/must-set-up-mfa.php @@ -0,0 +1,48 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+
+
+ <?= $this->t('{material:mfa:shield_icon}') ?> +
+ +
+

+ t('{material:mfa:required_header}') ?> +

+
+ +
+

+ t('{material:mfa:required_info}') ?> +

+
+ +
+ + + +
+
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/new-backup-codes.php b/modules/material/themes/material/mfa/new-backup-codes.php new file mode 100644 index 00000000..175f1548 --- /dev/null +++ b/modules/material/themes/material/mfa/new-backup-codes.php @@ -0,0 +1,158 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+ data['newBackupCodes']; ?> + +

+ t('{material:mfa:new_codes_header}') ?> +

+ +

+ t('{material:mfa:old_codes_gone}') ?> +

+ +

+ t('{material:mfa:new_codes_info}') ?> + t('{material:mfa:new_codes_only_once}') ?> +

+ +
+
+ configuration->getValue('idp_display_name', $this->configuration->getValue('idp_name', '—'))); + ?> +

+ t('{material:mfa:account}', ['{idpName}' => $idpName]) ?> + +

+ +
+ + + +
+ + t('{material:mfa:new_codes_only_once}') ?> +
+ +
+ + + + " + download="-printable-codes.txt" class="mdl-button mdl-button--primary"> + t('{material:mfa:button_download}') ?> + + + + +
+
+ +
+
+ error +
+ +
+

+ t('{material:error:header}') ?> +

+
+ +
+

+ t('{material:mfa:new_codes_failed}') ?> + data['mfaSetupUrl'] ?> +

+
+
+ + + + +
+ + + +
+ + +
+
+
+ + diff --git a/modules/material/themes/material/mfa/other_mfas.php b/modules/material/themes/material/mfa/other_mfas.php new file mode 100644 index 00000000..199336ca --- /dev/null +++ b/modules/material/themes/material/mfa/other_mfas.php @@ -0,0 +1,58 @@ +data['mfaOptions']; +$currentMfaId = filter_input(INPUT_GET, 'mfaId'); + +function excludeSelf($others, $selfId) { + return array_filter($others, function($option) use ($selfId) { + return $option['id'] != $selfId; + }); +} + +$otherOptions = excludeSelf($mfaOptions, $currentMfaId); + +if (! empty($this->data['managerEmail'])) { + $otherOptions[] = [ + 'type' => 'manager', + 'callback' => '/module.php/mfa/send-manager-mfa.php?StateId='.htmlentities($this->data['stateId']) + ]; +} + +if (count($otherOptions) > 0) { +?> +
+ + +
    + data['stateId']).'&mfaId='.htmlentities($option['id']); + + $image = 'mfa-' . $type . '.svg'; + $altText = $this->t('{material:mfa:' . $type . '_icon}'); + ?> +
  • + + <?= $altText ?> + + + t('{material:mfa:use_' . $label . '}') ?> + +
  • + +
+
+ diff --git a/modules/material/themes/material/mfa/out-of-backup-codes.php b/modules/material/themes/material/mfa/out-of-backup-codes.php new file mode 100644 index 00000000..cc4db6cc --- /dev/null +++ b/modules/material/themes/material/mfa/out-of-backup-codes.php @@ -0,0 +1,58 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+
+
+ error +
+ +
+

+ t('{material:mfa:no_more_codes_header}') ?> +

+
+ +
+

+ data['hasOtherMfaOptions']): ?> + t('{material:mfa:has_options_besides_codes}') ?> + + t('{material:mfa:has_no_more_options}') ?> + +

+
+ +
+ data['hasOtherMfaOptions']): ?> + + + + + + +
+
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php b/modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php new file mode 100644 index 00000000..a621d002 --- /dev/null +++ b/modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php @@ -0,0 +1,92 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+
+
+ <?= $this->t('{material:mfa:backupcode_icon}') ?> +
+ +
+

+ t('{material:mfa:backupcode_header}') ?> +

+
+ +
+

+ t('{material:mfa:backupcode_reminder}') ?> +

+
+ +
+
+ + +
+
+ + data['errorMessage']; + + if (! empty($message)) { + ?> +
+

+ error + + + + +

+
+ + + + +
+ + +
+ + +
+ +
+ +
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-manager.php b/modules/material/themes/material/mfa/prompt-for-mfa-manager.php new file mode 100644 index 00000000..a385a082 --- /dev/null +++ b/modules/material/themes/material/mfa/prompt-for-mfa-manager.php @@ -0,0 +1,89 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+
+
+ <?= $this->t('{material:mfa:manager_icon}') ?> +
+ +
+

+ t('{material:mfa:manager_header}') ?> +

+
+ +
+

+ t('{material:mfa:manager_sent}', ['{managerEmail}' => $this->data['managerEmail']]) ?> +

+
+ +
+
+ + +
+
+ + data['errorMessage']; + + if (! empty($message)) { + ?> +
+

+ error + + + + +

+
+ + + + +
+ + +
+ + +
+ +
+ +
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-totp.php b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php new file mode 100644 index 00000000..4b861524 --- /dev/null +++ b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php @@ -0,0 +1,92 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+
+
+ <?= $this->t('{material:mfa:totp_icon}') ?> +
+ +
+

+ t('{material:mfa:totp_header}') ?> +

+
+ +
+

+ t('{material:mfa:totp_instructions}') ?> +

+
+ +
+
+ + +
+
+ + data['errorMessage']; + + if (! empty($message)) { + ?> +
+

+ error + + + + +

+
+ + + + +
+ + +
+ + +
+ +
+ +
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-u2f.php b/modules/material/themes/material/mfa/prompt-for-mfa-u2f.php new file mode 100644 index 00000000..bc867991 --- /dev/null +++ b/modules/material/themes/material/mfa/prompt-for-mfa-u2f.php @@ -0,0 +1,175 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + + + + + +data['supportsU2f']; ?> + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+ +
+
+
+
+ <?= $this->t('{material:mfa:u2f_icon}') ?> +
+ +
+

+ t('{material:mfa:u2f_header}') ?> +

+
+ + +
+

+ t('{material:mfa:u2f_instructions}') ?> +

+
+ +
+

+ t('{material:mfa:u2f_unsupported}') ?> +

+
+ + + data['errorMessage']; + if (! empty($message)) { + ?> + + +
+

+ error + + + + +

+
+ +
+ + + + +
+ + +
+ +
+ +
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-webauthn.php b/modules/material/themes/material/mfa/prompt-for-mfa-webauthn.php new file mode 100644 index 00000000..7f8ccbe2 --- /dev/null +++ b/modules/material/themes/material/mfa/prompt-for-mfa-webauthn.php @@ -0,0 +1,157 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + + + + + + +data['supportsWebAuthn']; ?> + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+ +
+
+
+
+ <?= $this->t('{material:mfa:webauthn_icon}') ?> +
+ +
+

+ t('{material:mfa:webauthn_header}') ?> +

+
+ + +
+

+ t('{material:mfa:webauthn_instructions}') ?> +

+
+ +
+

+ t('{material:mfa:webauthn_unsupported}') ?> +

+
+ + + data['errorMessage']; + if (! empty($message)) { + ?> + + +
+

+ error + + + + +

+
+ +
+ + + + +
+ + +
+ +
+ +
+
+
+
+ + diff --git a/modules/material/themes/material/mfa/send-manager-mfa.php b/modules/material/themes/material/mfa/send-manager-mfa.php new file mode 100644 index 00000000..8bb30a8c --- /dev/null +++ b/modules/material/themes/material/mfa/send-manager-mfa.php @@ -0,0 +1,51 @@ + + + + <?= $this->t('{material:mfa:title}') ?> + + + + +
+
+
+ + t('{material:mfa:header}') ?> + +
+
+
+
+
+
+ <?= $this->t('{material:mfa:manager_icon}') ?> +
+ +
+

+ t('{material:mfa:manager_header}') ?> +

+
+ +
+

+ t('{material:mfa:manager_info}', ['{managerEmail}' => $this->data['managerEmail']]) ?> +

+
+ +
+ + + + +
+
+
+
+
+ + diff --git a/modules/material/themes/material/profilereview/nag-for-method.php b/modules/material/themes/material/profilereview/nag-for-method.php new file mode 100644 index 00000000..7e6d50d1 --- /dev/null +++ b/modules/material/themes/material/profilereview/nag-for-method.php @@ -0,0 +1,52 @@ + + + + <?= $this->t('{material:nag:method_title}') ?> + + + + +
+
+
+ + t('{material:nag:method_header}') ?> + +
+
+
+
+
+
+ <?= $this->t('{material:nag:shield_icon}') ?> +
+ +
+

+ t('{material:nag:header}') ?> +

+
+ +
+

+ t('{material:nag:method_info}') ?> +

+
+ +
+ + + + + +
+
+
+
+
+ + diff --git a/modules/material/themes/material/profilereview/nag-for-mfa.php b/modules/material/themes/material/profilereview/nag-for-mfa.php new file mode 100644 index 00000000..e18c9964 --- /dev/null +++ b/modules/material/themes/material/profilereview/nag-for-mfa.php @@ -0,0 +1,63 @@ + + + + <?= $this->t('{material:nag:mfa_title}') ?> + + + + +
+
+
+ + t('{material:nag:mfa_header}') ?> + +
+
+
+
+
+
+ <?= $this->t('{material:nag:shield_icon}') ?> +
+ +
+

+ t('{material:nag:header}') ?> +

+
+ +
+

+ t('{material:nag:mfa_info}') ?> +

+
+ +
+ + + + + data['mfaLearnMoreUrl'] ?? null; + if (! empty($url)) { + ?> + + t('{material:nag:button_learn_more}') ?> + + + + +
+
+
+
+
+ + diff --git a/modules/material/themes/material/profilereview/review.php b/modules/material/themes/material/profilereview/review.php new file mode 100644 index 00000000..039f4692 --- /dev/null +++ b/modules/material/themes/material/profilereview/review.php @@ -0,0 +1,128 @@ + + + + <?= $this->t('{material:review:title}') ?> + + + + + + +
+
+
+ + t('{material:review:header}') ?> + +
+
+ +
+
+

+

+ t('{material:review:info}') ?> +

+

+ +
+ data['mfaOptions']) > 0): ?> +
+
+

+ t('{material:review:mfa_header}') ?> +

+
+ +
+
    + data['mfaOptions'] as $mfa): ?> +
  • + + + + + + + t('{material:review:remaining}', ['{count}' => (string) $mfa['data']['count']]) ?> + + + + + t('{material:review:used_never}') ?> + + t('{material:review:used}', ['{when}' => $mfa['last_used_utc']]) ?> + + + +
  • + +
+
+
+ + + data['methodOptions']) > 0): ?> + +
+
+

+ t('{material:review:methods_header}') ?> +

+
+ +
+
    + data['methodOptions'] as $method): ?> +
  • + + + + + + + t('{material:review:'.($method['verified'] ? 'verified' : 'unverified').'}') ?> + + +
  • + +
+
+
+ +
+ +
+ + t('{material:review:button_update}') ?> launch + + + +
+
+
+ + +
+ + diff --git a/modules/material/www/bowser.1.9.4.min.js b/modules/material/www/bowser.1.9.4.min.js new file mode 100644 index 00000000..5c5d3733 --- /dev/null +++ b/modules/material/www/bowser.1.9.4.min.js @@ -0,0 +1,6 @@ +/*! + * Bowser - a browser detector + * https://github.com/ded/bowser + * MIT License | (c) Dustin Diaz 2015 + */ +!function(e,t,n){typeof module!="undefined"&&module.exports?module.exports=n():typeof define=="function"&&define.amd?define(t,n):e[t]=n()}(this,"bowser",function(){function t(t){function n(e){var n=t.match(e);return n&&n.length>1&&n[1]||""}function r(e){var n=t.match(e);return n&&n.length>1&&n[2]||""}function C(e){switch(e){case"NT":return"NT";case"XP":return"XP";case"NT 5.0":return"2000";case"NT 5.1":return"XP";case"NT 5.2":return"2003";case"NT 6.0":return"Vista";case"NT 6.1":return"7";case"NT 6.2":return"8";case"NT 6.3":return"8.1";case"NT 10.0":return"10";default:return undefined}}var i=n(/(ipod|iphone|ipad)/i).toLowerCase(),o=/like android/i.test(t),u=!o&&/android/i.test(t),a=/nexus\s*[0-6]\s*/i.test(t),f=!a&&/nexus\s*[0-9]+/i.test(t),l=/CrOS/.test(t),c=/silk/i.test(t),h=/sailfish/i.test(t),p=/tizen/i.test(t),d=/(web|hpw)(o|0)s/i.test(t),v=/windows phone/i.test(t),m=/SamsungBrowser/i.test(t),g=!v&&/windows/i.test(t),y=!i&&!c&&/macintosh/i.test(t),b=!u&&!h&&!p&&!d&&/linux/i.test(t),w=r(/edg([ea]|ios)\/(\d+(\.\d+)?)/i),E=n(/version\/(\d+(\.\d+)?)/i),S=/tablet/i.test(t)&&!/tablet pc/i.test(t),x=!S&&/[^-]mobi/i.test(t),T=/xbox/i.test(t),N;/opera/i.test(t)?N={name:"Opera",opera:e,version:E||n(/(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i)}:/opr\/|opios/i.test(t)?N={name:"Opera",opera:e,version:n(/(?:opr|opios)[\s\/](\d+(\.\d+)?)/i)||E}:/SamsungBrowser/i.test(t)?N={name:"Samsung Internet for Android",samsungBrowser:e,version:E||n(/(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i)}:/Whale/i.test(t)?N={name:"NAVER Whale browser",whale:e,version:n(/(?:whale)[\s\/](\d+(?:\.\d+)+)/i)}:/MZBrowser/i.test(t)?N={name:"MZ Browser",mzbrowser:e,version:n(/(?:MZBrowser)[\s\/](\d+(?:\.\d+)+)/i)}:/coast/i.test(t)?N={name:"Opera Coast",coast:e,version:E||n(/(?:coast)[\s\/](\d+(\.\d+)?)/i)}:/focus/i.test(t)?N={name:"Focus",focus:e,version:n(/(?:focus)[\s\/](\d+(?:\.\d+)+)/i)}:/yabrowser/i.test(t)?N={name:"Yandex Browser",yandexbrowser:e,version:E||n(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)}:/ucbrowser/i.test(t)?N={name:"UC Browser",ucbrowser:e,version:n(/(?:ucbrowser)[\s\/](\d+(?:\.\d+)+)/i)}:/mxios/i.test(t)?N={name:"Maxthon",maxthon:e,version:n(/(?:mxios)[\s\/](\d+(?:\.\d+)+)/i)}:/epiphany/i.test(t)?N={name:"Epiphany",epiphany:e,version:n(/(?:epiphany)[\s\/](\d+(?:\.\d+)+)/i)}:/puffin/i.test(t)?N={name:"Puffin",puffin:e,version:n(/(?:puffin)[\s\/](\d+(?:\.\d+)?)/i)}:/sleipnir/i.test(t)?N={name:"Sleipnir",sleipnir:e,version:n(/(?:sleipnir)[\s\/](\d+(?:\.\d+)+)/i)}:/k-meleon/i.test(t)?N={name:"K-Meleon",kMeleon:e,version:n(/(?:k-meleon)[\s\/](\d+(?:\.\d+)+)/i)}:v?(N={name:"Windows Phone",osname:"Windows Phone",windowsphone:e},w?(N.msedge=e,N.version=w):(N.msie=e,N.version=n(/iemobile\/(\d+(\.\d+)?)/i))):/msie|trident/i.test(t)?N={name:"Internet Explorer",msie:e,version:n(/(?:msie |rv:)(\d+(\.\d+)?)/i)}:l?N={name:"Chrome",osname:"Chrome OS",chromeos:e,chromeBook:e,chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:/edg([ea]|ios)/i.test(t)?N={name:"Microsoft Edge",msedge:e,version:w}:/vivaldi/i.test(t)?N={name:"Vivaldi",vivaldi:e,version:n(/vivaldi\/(\d+(\.\d+)?)/i)||E}:h?N={name:"Sailfish",osname:"Sailfish OS",sailfish:e,version:n(/sailfish\s?browser\/(\d+(\.\d+)?)/i)}:/seamonkey\//i.test(t)?N={name:"SeaMonkey",seamonkey:e,version:n(/seamonkey\/(\d+(\.\d+)?)/i)}:/firefox|iceweasel|fxios/i.test(t)?(N={name:"Firefox",firefox:e,version:n(/(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i)},/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(t)&&(N.firefoxos=e,N.osname="Firefox OS")):c?N={name:"Amazon Silk",silk:e,version:n(/silk\/(\d+(\.\d+)?)/i)}:/phantom/i.test(t)?N={name:"PhantomJS",phantom:e,version:n(/phantomjs\/(\d+(\.\d+)?)/i)}:/slimerjs/i.test(t)?N={name:"SlimerJS",slimer:e,version:n(/slimerjs\/(\d+(\.\d+)?)/i)}:/blackberry|\bbb\d+/i.test(t)||/rim\stablet/i.test(t)?N={name:"BlackBerry",osname:"BlackBerry OS",blackberry:e,version:E||n(/blackberry[\d]+\/(\d+(\.\d+)?)/i)}:d?(N={name:"WebOS",osname:"WebOS",webos:e,version:E||n(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)},/touchpad\//i.test(t)&&(N.touchpad=e)):/bada/i.test(t)?N={name:"Bada",osname:"Bada",bada:e,version:n(/dolfin\/(\d+(\.\d+)?)/i)}:p?N={name:"Tizen",osname:"Tizen",tizen:e,version:n(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i)||E}:/qupzilla/i.test(t)?N={name:"QupZilla",qupzilla:e,version:n(/(?:qupzilla)[\s\/](\d+(?:\.\d+)+)/i)||E}:/chromium/i.test(t)?N={name:"Chromium",chromium:e,version:n(/(?:chromium)[\s\/](\d+(?:\.\d+)?)/i)||E}:/chrome|crios|crmo/i.test(t)?N={name:"Chrome",chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:u?N={name:"Android",version:E}:/safari|applewebkit/i.test(t)?(N={name:"Safari",safari:e},E&&(N.version=E)):i?(N={name:i=="iphone"?"iPhone":i=="ipad"?"iPad":"iPod"},E&&(N.version=E)):/googlebot/i.test(t)?N={name:"Googlebot",googlebot:e,version:n(/googlebot\/(\d+(\.\d+))/i)||E}:N={name:n(/^(.*)\/(.*) /),version:r(/^(.*)\/(.*) /)},!N.msedge&&/(apple)?webkit/i.test(t)?(/(apple)?webkit\/537\.36/i.test(t)?(N.name=N.name||"Blink",N.blink=e):(N.name=N.name||"Webkit",N.webkit=e),!N.version&&E&&(N.version=E)):!N.opera&&/gecko\//i.test(t)&&(N.name=N.name||"Gecko",N.gecko=e,N.version=N.version||n(/gecko\/(\d+(\.\d+)?)/i)),!N.windowsphone&&(u||N.silk)?(N.android=e,N.osname="Android"):!N.windowsphone&&i?(N[i]=e,N.ios=e,N.osname="iOS"):y?(N.mac=e,N.osname="macOS"):T?(N.xbox=e,N.osname="Xbox"):g?(N.windows=e,N.osname="Windows"):b&&(N.linux=e,N.osname="Linux");var k="";N.windows?k=C(n(/Windows ((NT|XP)( \d\d?.\d)?)/i)):N.windowsphone?k=n(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i):N.mac?(k=n(/Mac OS X (\d+([_\.\s]\d+)*)/i),k=k.replace(/[_\s]/g,".")):i?(k=n(/os (\d+([_\s]\d+)*) like mac os x/i),k=k.replace(/[_\s]/g,".")):u?k=n(/android[ \/-](\d+(\.\d+)*)/i):N.webos?k=n(/(?:web|hpw)os\/(\d+(\.\d+)*)/i):N.blackberry?k=n(/rim\stablet\sos\s(\d+(\.\d+)*)/i):N.bada?k=n(/bada\/(\d+(\.\d+)*)/i):N.tizen&&(k=n(/tizen[\/\s](\d+(\.\d+)*)/i)),k&&(N.osversion=k);var L=!N.windows&&k.split(".")[0];if(S||f||i=="ipad"||u&&(L==3||L>=4&&!x)||N.silk)N.tablet=e;else if(x||i=="iphone"||i=="ipod"||u||a||N.blackberry||N.webos||N.bada)N.mobile=e;return N.msedge||N.msie&&N.version>=10||N.yandexbrowser&&N.version>=15||N.vivaldi&&N.version>=1||N.chrome&&N.version>=20||N.samsungBrowser&&N.version>=4||N.whale&&s([N.version,"1.0"])===1||N.mzbrowser&&s([N.version,"6.0"])===1||N.focus&&s([N.version,"1.0"])===1||N.firefox&&N.version>=20||N.safari&&N.version>=6||N.opera&&N.version>=10||N.ios&&N.osversion&&N.osversion.split(".")[0]>=6||N.blackberry&&N.version>=10.1||N.chromium&&N.version>=20?N.a=e:N.msie&&N.version<10||N.chrome&&N.version<20||N.firefox&&N.version<20||N.safari&&N.version<6||N.opera&&N.version<10||N.ios&&N.osversion&&N.osversion.split(".")[0]<6||N.chromium&&N.version<20?N.c=e:N.x=e,N}function r(e){return e.split(".").length}function i(e,t){var n=[],r;if(Array.prototype.map)return Array.prototype.map.call(e,t);for(r=0;r=0){if(n[0][t]>n[1][t])return 1;if(n[0][t]!==n[1][t])return-1;if(t===0)return 0}}function o(e,r,i){var o=n;typeof r=="string"&&(i=r,r=void 0),r===void 0&&(r=!1),i&&(o=t(i));var u=""+o.version;for(var a in e)if(e.hasOwnProperty(a)&&o[a]){if(typeof e[a]!="string")throw new Error("Browser version in the minVersion map should be a string: "+a+": "+String(e));return s([u,e[a]])<0}return r}function u(e,t,n){return!o(e,t,n)}var e=!0,n=t(typeof navigator!="undefined"?navigator.userAgent||"":"");return n.test=function(e){for(var t=0;t#9f?b)0?(;2uFYmp* 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 \ No newline at end of file diff --git a/modules/material/www/material-icons.woff b/modules/material/www/material-icons.woff new file mode 100644 index 0000000000000000000000000000000000000000..e2cd4f1ba16de1acfeebc00b1dc56635899f93f4 GIT binary patch literal 61928 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 C24P$P literal 0 HcmV?d00001 diff --git a/modules/material/www/material-icons.woff2 b/modules/material/www/material-icons.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..dfad0dcf2d82d194b24a919d025734718241e961 GIT binary patch literal 46740 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.children))}function l(t){var s="undefined"==typeof t.widget&&"undefined"==typeof t.widget,i=!0;s||(i=t.widget||t.widget);var n={classConstructor:t.constructor||t.constructor,className:t.classAsString||t.classAsString,cssClass:t.cssClass||t.cssClass,widget:i,callbacks:[]};if(h.forEach(function(e){if(e.cssClass===n.cssClass)throw new Error("The provided cssClass has already been registered: "+e.cssClass);if(e.className===n.className)throw new Error("The provided className has already been registered")}),t.constructor.prototype.hasOwnProperty(p))throw new Error("MDL component classes must not have "+p+" defined as a property.");var a=e(t.classAsString,n);a||h.push(n)}function o(t,s){var i=e(t);i&&i.callbacks.push(s)}function r(){for(var e=0;e0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)&&(e.keyCode===this.Keycodes_.UP_ARROW?(e.preventDefault(),t[t.length-1].focus()):e.keyCode===this.Keycodes_.DOWN_ARROW&&(e.preventDefault(),t[0].focus()))}},d.prototype.handleItemKeyboardEvent_=function(e){if(this.element_&&this.container_){var t=this.element_.querySelectorAll("."+this.CssClasses_.ITEM+":not([disabled])");if(t&&t.length>0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)){var s=Array.prototype.slice.call(t).indexOf(e.target);if(e.keyCode===this.Keycodes_.UP_ARROW)e.preventDefault(),s>0?t[s-1].focus():t[t.length-1].focus();else if(e.keyCode===this.Keycodes_.DOWN_ARROW)e.preventDefault(),t.length>s+1?t[s+1].focus():t[0].focus();else if(e.keyCode===this.Keycodes_.SPACE||e.keyCode===this.Keycodes_.ENTER){e.preventDefault();var i=new MouseEvent("mousedown");e.target.dispatchEvent(i),i=new MouseEvent("mouseup"),e.target.dispatchEvent(i),e.target.click()}else e.keyCode===this.Keycodes_.ESCAPE&&(e.preventDefault(),this.hide())}}},d.prototype.handleItemClick_=function(e){e.target.hasAttribute("disabled")?e.stopPropagation():(this.closing_=!0,window.setTimeout(function(e){this.hide(),this.closing_=!1}.bind(this),this.Constant_.CLOSE_TIMEOUT))},d.prototype.applyClip_=function(e,t){this.element_.classList.contains(this.CssClasses_.UNALIGNED)?this.element_.style.clip="":this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)?this.element_.style.clip="rect(0 "+t+"px 0 "+t+"px)":this.element_.classList.contains(this.CssClasses_.TOP_LEFT)?this.element_.style.clip="rect("+e+"px 0 "+e+"px 0)":this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)?this.element_.style.clip="rect("+e+"px "+t+"px "+e+"px "+t+"px)":this.element_.style.clip=""},d.prototype.removeAnimationEndListener_=function(e){e.target.classList.remove(d.prototype.CssClasses_.IS_ANIMATING)},d.prototype.addAnimationEndListener_=function(){this.element_.addEventListener("transitionend",this.removeAnimationEndListener_),this.element_.addEventListener("webkitTransitionEnd",this.removeAnimationEndListener_)},d.prototype.show=function(e){if(this.element_&&this.container_&&this.outline_){var t=this.element_.getBoundingClientRect().height,s=this.element_.getBoundingClientRect().width;this.container_.style.width=s+"px",this.container_.style.height=t+"px",this.outline_.style.width=s+"px",this.outline_.style.height=t+"px";for(var i=this.Constant_.TRANSITION_DURATION_SECONDS*this.Constant_.TRANSITION_DURATION_FRACTION,n=this.element_.querySelectorAll("."+this.CssClasses_.ITEM),a=0;a0&&this.showSnackbar(this.queuedNotifications_.shift())},C.prototype.cleanup_=function(){this.element_.classList.remove(this.cssClasses_.ACTIVE),setTimeout(function(){this.element_.setAttribute("aria-hidden","true"),this.textElement_.textContent="",Boolean(this.actionElement_.getAttribute("aria-hidden"))||(this.setActionHidden_(!0),this.actionElement_.textContent="",this.actionElement_.removeEventListener("click",this.actionHandler_)),this.actionHandler_=void 0,this.message_=void 0,this.actionText_=void 0,this.active=!1,this.checkQueue_()}.bind(this),this.Constant_.ANIMATION_LENGTH)},C.prototype.setActionHidden_=function(e){e?this.actionElement_.setAttribute("aria-hidden","true"):this.actionElement_.removeAttribute("aria-hidden")},s.register({constructor:C,classAsString:"MaterialSnackbar",cssClass:"mdl-js-snackbar",widget:!0});var u=function(e){this.element_=e,this.init()};window.MaterialSpinner=u,u.prototype.Constant_={MDL_SPINNER_LAYER_COUNT:4},u.prototype.CssClasses_={MDL_SPINNER_LAYER:"mdl-spinner__layer",MDL_SPINNER_CIRCLE_CLIPPER:"mdl-spinner__circle-clipper",MDL_SPINNER_CIRCLE:"mdl-spinner__circle",MDL_SPINNER_GAP_PATCH:"mdl-spinner__gap-patch",MDL_SPINNER_LEFT:"mdl-spinner__left",MDL_SPINNER_RIGHT:"mdl-spinner__right"},u.prototype.createLayer=function(e){var t=document.createElement("div");t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER),t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER+"-"+e);var s=document.createElement("div");s.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),s.classList.add(this.CssClasses_.MDL_SPINNER_LEFT);var i=document.createElement("div");i.classList.add(this.CssClasses_.MDL_SPINNER_GAP_PATCH);var n=document.createElement("div");n.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),n.classList.add(this.CssClasses_.MDL_SPINNER_RIGHT);for(var a=[s,i,n],l=0;l=this.maxRows&&e.preventDefault()},L.prototype.onFocus_=function(e){this.element_.classList.add(this.CssClasses_.IS_FOCUSED)},L.prototype.onBlur_=function(e){this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.onReset_=function(e){this.updateClasses_()},L.prototype.updateClasses_=function(){this.checkDisabled(),this.checkValidity(),this.checkDirty(),this.checkFocus()},L.prototype.checkDisabled=function(){this.input_.disabled?this.element_.classList.add(this.CssClasses_.IS_DISABLED):this.element_.classList.remove(this.CssClasses_.IS_DISABLED)},L.prototype.checkDisabled=L.prototype.checkDisabled,L.prototype.checkFocus=function(){Boolean(this.element_.querySelector(":focus"))?this.element_.classList.add(this.CssClasses_.IS_FOCUSED):this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.checkFocus=L.prototype.checkFocus,L.prototype.checkValidity=function(){this.input_.validity&&(this.input_.validity.valid?this.element_.classList.remove(this.CssClasses_.IS_INVALID):this.element_.classList.add(this.CssClasses_.IS_INVALID))},L.prototype.checkValidity=L.prototype.checkValidity,L.prototype.checkDirty=function(){this.input_.value&&this.input_.value.length>0?this.element_.classList.add(this.CssClasses_.IS_DIRTY):this.element_.classList.remove(this.CssClasses_.IS_DIRTY)},L.prototype.checkDirty=L.prototype.checkDirty,L.prototype.disable=function(){this.input_.disabled=!0,this.updateClasses_()},L.prototype.disable=L.prototype.disable,L.prototype.enable=function(){this.input_.disabled=!1,this.updateClasses_()},L.prototype.enable=L.prototype.enable,L.prototype.change=function(e){this.input_.value=e||"",this.updateClasses_()},L.prototype.change=L.prototype.change,L.prototype.init=function(){if(this.element_&&(this.label_=this.element_.querySelector("."+this.CssClasses_.LABEL),this.input_=this.element_.querySelector("."+this.CssClasses_.INPUT),this.input_)){this.input_.hasAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE)&&(this.maxRows=parseInt(this.input_.getAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE),10),isNaN(this.maxRows)&&(this.maxRows=this.Constant_.NO_MAX_ROWS)),this.input_.hasAttribute("placeholder")&&this.element_.classList.add(this.CssClasses_.HAS_PLACEHOLDER),this.boundUpdateClassesHandler=this.updateClasses_.bind(this),this.boundFocusHandler=this.onFocus_.bind(this),this.boundBlurHandler=this.onBlur_.bind(this),this.boundResetHandler=this.onReset_.bind(this),this.input_.addEventListener("input",this.boundUpdateClassesHandler),this.input_.addEventListener("focus",this.boundFocusHandler),this.input_.addEventListener("blur",this.boundBlurHandler),this.input_.addEventListener("reset",this.boundResetHandler),this.maxRows!==this.Constant_.NO_MAX_ROWS&&(this.boundKeyDownHandler=this.onKeyDown_.bind(this),this.input_.addEventListener("keydown",this.boundKeyDownHandler));var e=this.element_.classList.contains(this.CssClasses_.IS_INVALID);this.updateClasses_(),this.element_.classList.add(this.CssClasses_.IS_UPGRADED),e&&this.element_.classList.add(this.CssClasses_.IS_INVALID),this.input_.hasAttribute("autofocus")&&(this.element_.focus(),this.checkFocus())}},s.register({constructor:L,classAsString:"MaterialTextfield",cssClass:"mdl-js-textfield",widget:!0});var I=function(e){this.element_=e,this.init()};window.MaterialTooltip=I,I.prototype.Constant_={},I.prototype.CssClasses_={IS_ACTIVE:"is-active",BOTTOM:"mdl-tooltip--bottom",LEFT:"mdl-tooltip--left",RIGHT:"mdl-tooltip--right",TOP:"mdl-tooltip--top"},I.prototype.handleMouseEnter_=function(e){var t=e.target.getBoundingClientRect(),s=t.left+t.width/2,i=t.top+t.height/2,n=-1*(this.element_.offsetWidth/2),a=-1*(this.element_.offsetHeight/2);this.element_.classList.contains(this.CssClasses_.LEFT)||this.element_.classList.contains(this.CssClasses_.RIGHT)?(s=t.width/2,i+a<0?(this.element_.style.top="0",this.element_.style.marginTop="0"):(this.element_.style.top=i+"px",this.element_.style.marginTop=a+"px")):s+n<0?(this.element_.style.left="0",this.element_.style.marginLeft="0"):(this.element_.style.left=s+"px",this.element_.style.marginLeft=n+"px"),this.element_.classList.contains(this.CssClasses_.TOP)?this.element_.style.top=t.top-this.element_.offsetHeight-10+"px":this.element_.classList.contains(this.CssClasses_.RIGHT)?this.element_.style.left=t.left+t.width+10+"px":this.element_.classList.contains(this.CssClasses_.LEFT)?this.element_.style.left=t.left-this.element_.offsetWidth-10+"px":this.element_.style.top=t.top+t.height+10+"px",this.element_.classList.add(this.CssClasses_.IS_ACTIVE)},I.prototype.hideTooltip_=function(){this.element_.classList.remove(this.CssClasses_.IS_ACTIVE)},I.prototype.init=function(){if(this.element_){var e=this.element_.getAttribute("for")||this.element_.getAttribute("data-mdl-for");e&&(this.forElement_=document.getElementById(e)),this.forElement_&&(this.forElement_.hasAttribute("tabindex")||this.forElement_.setAttribute("tabindex","0"),this.boundMouseEnterHandler=this.handleMouseEnter_.bind(this),this.boundMouseLeaveAndScrollHandler=this.hideTooltip_.bind(this),this.forElement_.addEventListener("mouseenter",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("touchend",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("mouseleave",this.boundMouseLeaveAndScrollHandler,!1),window.addEventListener("scroll",this.boundMouseLeaveAndScrollHandler,!0),window.addEventListener("touchstart",this.boundMouseLeaveAndScrollHandler))}},s.register({constructor:I,classAsString:"MaterialTooltip",cssClass:"mdl-tooltip"});var f=function(e){this.element_=e,this.init()};window.MaterialLayout=f,f.prototype.Constant_={MAX_WIDTH:"(max-width: 1024px)",TAB_SCROLL_PIXELS:100,RESIZE_TIMEOUT:100,MENU_ICON:"",CHEVRON_LEFT:"chevron_left",CHEVRON_RIGHT:"chevron_right"},f.prototype.Keycodes_={ENTER:13,ESCAPE:27,SPACE:32},f.prototype.Mode_={STANDARD:0,SEAMED:1,WATERFALL:2,SCROLL:3},f.prototype.CssClasses_={CONTAINER:"mdl-layout__container",HEADER:"mdl-layout__header",DRAWER:"mdl-layout__drawer",CONTENT:"mdl-layout__content",DRAWER_BTN:"mdl-layout__drawer-button",ICON:"material-icons",JS_RIPPLE_EFFECT:"mdl-js-ripple-effect",RIPPLE_CONTAINER:"mdl-layout__tab-ripple-container",RIPPLE:"mdl-ripple",RIPPLE_IGNORE_EVENTS:"mdl-js-ripple-effect--ignore-events",HEADER_SEAMED:"mdl-layout__header--seamed",HEADER_WATERFALL:"mdl-layout__header--waterfall",HEADER_SCROLL:"mdl-layout__header--scroll",FIXED_HEADER:"mdl-layout--fixed-header",OBFUSCATOR:"mdl-layout__obfuscator",TAB_BAR:"mdl-layout__tab-bar",TAB_CONTAINER:"mdl-layout__tab-bar-container",TAB:"mdl-layout__tab",TAB_BAR_BUTTON:"mdl-layout__tab-bar-button",TAB_BAR_LEFT_BUTTON:"mdl-layout__tab-bar-left-button",TAB_BAR_RIGHT_BUTTON:"mdl-layout__tab-bar-right-button",PANEL:"mdl-layout__tab-panel",HAS_DRAWER:"has-drawer",HAS_TABS:"has-tabs",HAS_SCROLLING_HEADER:"has-scrolling-header",CASTING_SHADOW:"is-casting-shadow",IS_COMPACT:"is-compact",IS_SMALL_SCREEN:"is-small-screen",IS_DRAWER_OPEN:"is-visible",IS_ACTIVE:"is-active",IS_UPGRADED:"is-upgraded",IS_ANIMATING:"is-animating",ON_LARGE_SCREEN:"mdl-layout--large-screen-only",ON_SMALL_SCREEN:"mdl-layout--small-screen-only"},f.prototype.contentScrollHandler_=function(){if(!this.header_.classList.contains(this.CssClasses_.IS_ANIMATING)){var e=!this.element_.classList.contains(this.CssClasses_.IS_SMALL_SCREEN)||this.element_.classList.contains(this.CssClasses_.FIXED_HEADER);this.content_.scrollTop>0&&!this.header_.classList.contains(this.CssClasses_.IS_COMPACT)?(this.header_.classList.add(this.CssClasses_.CASTING_SHADOW),this.header_.classList.add(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING)):this.content_.scrollTop<=0&&this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW),this.header_.classList.remove(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING))}},f.prototype.keyboardEventHandler_=function(e){e.keyCode===this.Keycodes_.ESCAPE&&this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)&&this.toggleDrawer()},f.prototype.screenSizeHandler_=function(){this.screenSizeMediaQuery_.matches?this.element_.classList.add(this.CssClasses_.IS_SMALL_SCREEN):(this.element_.classList.remove(this.CssClasses_.IS_SMALL_SCREEN),this.drawer_&&(this.drawer_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN),this.obfuscator_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN)))},f.prototype.drawerToggleHandler_=function(e){if(e&&"keydown"===e.type){if(e.keyCode!==this.Keycodes_.SPACE&&e.keyCode!==this.Keycodes_.ENTER)return;e.preventDefault()}this.toggleDrawer()},f.prototype.headerTransitionEndHandler_=function(){this.header_.classList.remove(this.CssClasses_.IS_ANIMATING)},f.prototype.headerClickHandler_=function(){this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.IS_COMPACT),this.header_.classList.add(this.CssClasses_.IS_ANIMATING))},f.prototype.resetTabState_=function(e){for(var t=0;t0?c.classList.add(this.CssClasses_.IS_ACTIVE):c.classList.remove(this.CssClasses_.IS_ACTIVE),this.tabBar_.scrollLeft0)return;this.setFrameCount(1);var i,n,a=e.currentTarget.getBoundingClientRect();if(0===e.clientX&&0===e.clientY)i=Math.round(a.width/2),n=Math.round(a.height/2);else{var l=e.clientX?e.clientX:e.touches[0].clientX,o=e.clientY?e.clientY:e.touches[0].clientY;i=Math.round(l-a.left),n=Math.round(o-a.top)}this.setRippleXY(i,n),this.setRippleStyles(!0),window.requestAnimationFrame(this.animFrameHandler.bind(this))}},y.prototype.upHandler_=function(e){e&&2!==e.detail&&window.setTimeout(function(){this.rippleElement_.classList.remove(this.CssClasses_.IS_VISIBLE)}.bind(this),0)},y.prototype.init=function(){if(this.element_){var e=this.element_.classList.contains(this.CssClasses_.RIPPLE_CENTER);this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT_IGNORE_EVENTS)||(this.rippleElement_=this.element_.querySelector("."+this.CssClasses_.RIPPLE),this.frameCount_=0,this.rippleSize_=0,this.x_=0,this.y_=0,this.ignoringMouseDown_=!1,this.boundDownHandler=this.downHandler_.bind(this),this.element_.addEventListener("mousedown",this.boundDownHandler),this.element_.addEventListener("touchstart",this.boundDownHandler),this.boundUpHandler=this.upHandler_.bind(this),this.element_.addEventListener("mouseup",this.boundUpHandler),this.element_.addEventListener("mouseleave",this.boundUpHandler),this.element_.addEventListener("touchend",this.boundUpHandler),this.element_.addEventListener("blur",this.boundUpHandler),this.getFrameCount=function(){return this.frameCount_},this.setFrameCount=function(e){this.frameCount_=e},this.getRippleElement=function(){return this.rippleElement_},this.setRippleXY=function(e,t){this.x_=e,this.y_=t},this.setRippleStyles=function(t){if(null!==this.rippleElement_){var s,i,n,a="translate("+this.x_+"px, "+this.y_+"px)";t?(i=this.Constant_.INITIAL_SCALE,n=this.Constant_.INITIAL_SIZE):(i=this.Constant_.FINAL_SCALE,n=this.rippleSize_+"px",e&&(a="translate("+this.boundWidth/2+"px, "+this.boundHeight/2+"px)")),s="translate(-50%, -50%) "+a+i,this.rippleElement_.style.webkitTransform=s,this.rippleElement_.style.msTransform=s,this.rippleElement_.style.transform=s,t?this.rippleElement_.classList.remove(this.CssClasses_.IS_ANIMATING):this.rippleElement_.classList.add(this.CssClasses_.IS_ANIMATING)}},this.animFrameHandler=function(){this.frameCount_-- >0?window.requestAnimationFrame(this.animFrameHandler.bind(this)):this.setRippleStyles(!1)})}},s.register({constructor:y,classAsString:"MaterialRipple",cssClass:"mdl-js-ripple-effect",widget:!1})}(); diff --git a/modules/material/www/material.blue_grey-teal.1.2.1.min.css b/modules/material/www/material.blue_grey-teal.1.2.1.min.css new file mode 100644 index 00000000..5450b339 --- /dev/null +++ b/modules/material/www/material.blue_grey-teal.1.2.1.min.css @@ -0,0 +1,8 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.2.1 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:rgb(100,255,218);font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:rgb(96,125,139)!important}.mdl-color--primary-contrast{background-color:rgb(255,255,255)!important}.mdl-color--primary-dark{background-color:rgb(69,90,100)!important}.mdl-color--accent{background-color:rgb(100,255,218)!important}.mdl-color--accent-contrast{background-color:rgb(66,66,66)!important}.mdl-color-text--primary{color:rgb(96,125,139)!important}.mdl-color-text--primary-contrast{color:rgb(255,255,255)!important}.mdl-color-text--primary-dark{color:rgb(69,90,100)!important}.mdl-color-text--accent{color:rgb(100,255,218)!important}.mdl-color-text--accent-contrast{color:rgb(66,66,66)!important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:rgb(100,255,218);color:rgb(66,66,66)}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:rgb(100,255,218);background:rgba(66,66,66,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(96,125,139)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(96,125,139);color:rgb(255,255,255)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(96,125,139)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(96,125,139)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(96,125,139)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(100,255,218);color:rgb(66,66,66)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(100,255,218)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(100,255,218)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(100,255,218)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(66,66,66)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(96,125,139)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(255,255,255)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(96,125,139)}.mdl-button--accent.mdl-button--accent{color:rgb(100,255,218)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(66,66,66)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(66,66,66);background-color:rgb(100,255,218)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:rgb(100,255,218);background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(96,125,139)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(96,125,139,.26);background-color:rgba(96,125,139,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(96,125,139)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(96,125,139)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-chip{height:32px;font-family:"Roboto","Helvetica","Arial",sans-serif;line-height:32px;padding:0 12px;border:0;border-radius:16px;background-color:#dedede;display:inline-block;color:rgba(0,0,0,.87);margin:2px 0;font-size:0;white-space:nowrap}.mdl-chip__text{font-size:13px;vertical-align:middle;display:inline-block}.mdl-chip__action{height:24px;width:24px;background:0 0;opacity:.54;cursor:pointer;padding:0;margin:0 0 0 4px;font-size:13px;text-decoration:none;color:rgba(0,0,0,.87);border:none;outline:none}.mdl-chip__action,.mdl-chip__contact{display:inline-block;vertical-align:middle;overflow:hidden;text-align:center}.mdl-chip__contact{height:32px;width:32px;border-radius:16px;margin-right:8px;font-size:18px;line-height:32px}.mdl-chip:focus{outline:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-chip:active{background-color:#d6d6d6}.mdl-chip--deletable{padding-right:4px}.mdl-chip--contact{padding-left:0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:rgb(96,125,139)}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(96,125,139,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(96,125,139);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(96,125,139),rgb(96,125,139));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(96,125,139),rgb(96,125,139));-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,rgb(96,125,139),rgb(96,125,139))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(96,125,139);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#e0e0e0;color:#000}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:56px;font-family:Helvetica,Arial,sans-serif;margin:8px 12px;top:0;left:0;color:rgb(255,255,255);z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:rgb(255,255,255);background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout__drawer-button{line-height:54px}.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button,.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:rgb(96,125,139);color:rgb(255,255,255);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:rgb(255,255,255);line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:rgb(96,125,139);overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:rgb(96,125,139);color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:rgb(255,255,255)}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:rgb(255,255,255)}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(100,255,218);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:rgb(255,255,255)}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid rgb(96,125,139)}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:rgb(96,125,139)}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:rgb(96,125,139)}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:rgb(96,125,139);-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,rgb(96,125,139)16px,rgb(96,125,139)0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:rgb(96,125,139);border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:rgb(96,125,139);border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(96,125,139,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(96,125,139,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:rgb(96,125,139);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:rgb(96,125,139);transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:rgb(96,125,139);transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgb(96,125,139)0%,rgb(96,125,139)37.5%,rgba(96,125,139,.26)37.5%,rgba(96,125,139,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:rgb(96,125,139);transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:rgb(96,125,139)}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:rgb(100,255,218);float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:rgb(96,125,139)}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:rgb(96,125,139)}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:rgb(96,125,139)}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:rgb(96,125,139)}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(96,125,139,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:rgb(96,125,139);left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(96,125,139,.26);background-color:rgba(96,125,139,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:rgb(96,125,139)}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(96,125,139);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:rgb(96,125,139)}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield--expandable .mdl-button--icon{top:16px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:rgb(96,125,139);font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:rgb(96,125,139);bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}body{margin:0}.styleguide-demo h1{margin:48px 24px 0}.styleguide-demo h1:after{content:'';display:block;width:100%;border-bottom:1px solid rgba(0,0,0,.5);margin-top:24px}.styleguide-demo{opacity:0;transition:opacity .6s ease}.styleguide-masthead{height:256px;background:#212121;padding:115px 16px 0}.styleguide-container{position:relative;max-width:960px;width:100%}.styleguide-title{color:#fff;bottom:auto;position:relative;font-size:56px;font-weight:300;line-height:1;letter-spacing:-.02em}.styleguide-title:after{border-bottom:0}.styleguide-title span{font-weight:300}.mdl-styleguide .mdl-layout__drawer .mdl-navigation__link{padding:10px 24px}.demosLoaded .styleguide-demo{opacity:1}iframe{display:block;width:100%;border:none}iframe.heightSet{overflow:hidden}.demo-wrapper{margin:24px}.demo-wrapper iframe{border:1px solid rgba(0,0,0,.5)} \ No newline at end of file diff --git a/modules/material/www/material.brown-orange.1.2.1.min.css b/modules/material/www/material.brown-orange.1.2.1.min.css new file mode 100644 index 00000000..f8b76e82 --- /dev/null +++ b/modules/material/www/material.brown-orange.1.2.1.min.css @@ -0,0 +1,8 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.2.1 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:rgb(255,171,64);font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:rgb(121,85,72)!important}.mdl-color--primary-contrast{background-color:rgb(255,255,255)!important}.mdl-color--primary-dark{background-color:rgb(93,64,55)!important}.mdl-color--accent{background-color:rgb(255,171,64)!important}.mdl-color--accent-contrast{background-color:rgb(66,66,66)!important}.mdl-color-text--primary{color:rgb(121,85,72)!important}.mdl-color-text--primary-contrast{color:rgb(255,255,255)!important}.mdl-color-text--primary-dark{color:rgb(93,64,55)!important}.mdl-color-text--accent{color:rgb(255,171,64)!important}.mdl-color-text--accent-contrast{color:rgb(66,66,66)!important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:rgb(255,171,64);color:rgb(66,66,66)}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:rgb(255,171,64);background:rgba(66,66,66,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(121,85,72)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(121,85,72);color:rgb(255,255,255)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(121,85,72)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(121,85,72)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(121,85,72)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(255,171,64);color:rgb(66,66,66)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(255,171,64)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(255,171,64)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(255,171,64)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(66,66,66)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(121,85,72)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(255,255,255)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(121,85,72)}.mdl-button--accent.mdl-button--accent{color:rgb(255,171,64)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(66,66,66)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(66,66,66);background-color:rgb(255,171,64)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:rgb(255,171,64);background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(121,85,72)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(121,85,72,.26);background-color:rgba(121,85,72,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(121,85,72)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(121,85,72)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-chip{height:32px;font-family:"Roboto","Helvetica","Arial",sans-serif;line-height:32px;padding:0 12px;border:0;border-radius:16px;background-color:#dedede;display:inline-block;color:rgba(0,0,0,.87);margin:2px 0;font-size:0;white-space:nowrap}.mdl-chip__text{font-size:13px;vertical-align:middle;display:inline-block}.mdl-chip__action{height:24px;width:24px;background:0 0;opacity:.54;cursor:pointer;padding:0;margin:0 0 0 4px;font-size:13px;text-decoration:none;color:rgba(0,0,0,.87);border:none;outline:none}.mdl-chip__action,.mdl-chip__contact{display:inline-block;vertical-align:middle;overflow:hidden;text-align:center}.mdl-chip__contact{height:32px;width:32px;border-radius:16px;margin-right:8px;font-size:18px;line-height:32px}.mdl-chip:focus{outline:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-chip:active{background-color:#d6d6d6}.mdl-chip--deletable{padding-right:4px}.mdl-chip--contact{padding-left:0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:rgb(121,85,72)}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(121,85,72,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(121,85,72);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(121,85,72),rgb(121,85,72));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(121,85,72),rgb(121,85,72));-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,rgb(121,85,72),rgb(121,85,72))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(121,85,72);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#e0e0e0;color:#000}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:56px;font-family:Helvetica,Arial,sans-serif;margin:8px 12px;top:0;left:0;color:rgb(255,255,255);z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:rgb(255,255,255);background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout__drawer-button{line-height:54px}.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button,.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:rgb(121,85,72);color:rgb(255,255,255);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:rgb(255,255,255);line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:rgb(121,85,72);overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:rgb(121,85,72);color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:rgb(255,255,255)}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:rgb(255,255,255)}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(255,171,64);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:rgb(255,255,255)}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid rgb(121,85,72)}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:rgb(121,85,72)}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:rgb(121,85,72)}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:rgb(121,85,72);-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,rgb(121,85,72)16px,rgb(121,85,72)0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:rgb(121,85,72);border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:rgb(121,85,72);border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(121,85,72,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(121,85,72,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:rgb(121,85,72);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:rgb(121,85,72);transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:rgb(121,85,72);transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgb(121,85,72)0%,rgb(121,85,72)37.5%,rgba(121,85,72,.26)37.5%,rgba(121,85,72,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:rgb(121,85,72);transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:rgb(121,85,72)}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:rgb(255,171,64);float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:rgb(121,85,72)}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:rgb(121,85,72)}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:rgb(121,85,72)}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:rgb(121,85,72)}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(121,85,72,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:rgb(121,85,72);left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(121,85,72,.26);background-color:rgba(121,85,72,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:rgb(121,85,72)}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(121,85,72);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:rgb(121,85,72)}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield--expandable .mdl-button--icon{top:16px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:rgb(121,85,72);font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:rgb(121,85,72);bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}body{margin:0}.styleguide-demo h1{margin:48px 24px 0}.styleguide-demo h1:after{content:'';display:block;width:100%;border-bottom:1px solid rgba(0,0,0,.5);margin-top:24px}.styleguide-demo{opacity:0;transition:opacity .6s ease}.styleguide-masthead{height:256px;background:#212121;padding:115px 16px 0}.styleguide-container{position:relative;max-width:960px;width:100%}.styleguide-title{color:#fff;bottom:auto;position:relative;font-size:56px;font-weight:300;line-height:1;letter-spacing:-.02em}.styleguide-title:after{border-bottom:0}.styleguide-title span{font-weight:300}.mdl-styleguide .mdl-layout__drawer .mdl-navigation__link{padding:10px 24px}.demosLoaded .styleguide-demo{opacity:1}iframe{display:block;width:100%;border:none}iframe.heightSet{overflow:hidden}.demo-wrapper{margin:24px}.demo-wrapper iframe{border:1px solid rgba(0,0,0,.5)} \ No newline at end of file diff --git a/modules/material/www/material.indigo-purple.1.2.1.min.css b/modules/material/www/material.indigo-purple.1.2.1.min.css new file mode 100644 index 00000000..be4c43b2 --- /dev/null +++ b/modules/material/www/material.indigo-purple.1.2.1.min.css @@ -0,0 +1,8 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.2.1 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:rgb(156,39,176);font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:rgb(63,81,181)!important}.mdl-color--primary-contrast{background-color:rgb(255,255,255)!important}.mdl-color--primary-dark{background-color:rgb(48,63,159)!important}.mdl-color--accent{background-color:rgb(224,64,251)!important}.mdl-color--accent-contrast{background-color:rgb(255,255,255)!important}.mdl-color-text--primary{color:rgb(63,81,181)!important}.mdl-color-text--primary-contrast{color:rgb(255,255,255)!important}.mdl-color-text--primary-dark{color:rgb(48,63,159)!important}.mdl-color-text--accent{color:rgb(156,39,176)!important}.mdl-color-text--accent-contrast{color:rgb(255,255,255)!important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:rgb(224,64,251);color:rgb(255,255,255)}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:rgb(224,64,251);background:rgba(255,255,255,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(63,81,181)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(63,81,181);color:rgb(255,255,255)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(63,81,181)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(63,81,181)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(63,81,181)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(224,64,251);color:rgb(255,255,255)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(224,64,251)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(224,64,251)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(224,64,251)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(63,81,181)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(255,255,255)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(63,81,181)}.mdl-button--accent.mdl-button--accent{color:rgb(224,64,251)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(255,255,255)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(224,64,251)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:rgb(224,64,251);background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(63,81,181)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(63,81,181)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(63,81,181)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-chip{height:32px;font-family:"Roboto","Helvetica","Arial",sans-serif;line-height:32px;padding:0 12px;border:0;border-radius:16px;background-color:#dedede;display:inline-block;color:rgba(0,0,0,.87);margin:2px 0;font-size:0;white-space:nowrap}.mdl-chip__text{font-size:13px;vertical-align:middle;display:inline-block}.mdl-chip__action{height:24px;width:24px;background:0 0;opacity:.54;cursor:pointer;padding:0;margin:0 0 0 4px;font-size:13px;text-decoration:none;color:rgba(0,0,0,.87);border:none;outline:none}.mdl-chip__action,.mdl-chip__contact{display:inline-block;vertical-align:middle;overflow:hidden;text-align:center}.mdl-chip__contact{height:32px;width:32px;border-radius:16px;margin-right:8px;font-size:18px;line-height:32px}.mdl-chip:focus{outline:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-chip:active{background-color:#d6d6d6}.mdl-chip--deletable{padding-right:4px}.mdl-chip--contact{padding-left:0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:rgb(63,81,181)}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(63,81,181,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(63,81,181);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(63,81,181),rgb(63,81,181));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(63,81,181),rgb(63,81,181));-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,rgb(63,81,181),rgb(63,81,181))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(63,81,181);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#e0e0e0;color:#000}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:56px;font-family:Helvetica,Arial,sans-serif;margin:8px 12px;top:0;left:0;color:rgb(255,255,255);z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:rgb(255,255,255);background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout__drawer-button{line-height:54px}.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button,.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:rgb(63,81,181);color:rgb(255,255,255);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:rgb(255,255,255);line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:rgb(63,81,181);overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:rgb(63,81,181);color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:rgb(255,255,255)}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:rgb(255,255,255)}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(224,64,251);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:rgb(255,255,255)}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid rgb(63,81,181)}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:rgb(63,81,181)}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:rgb(63,81,181)}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:rgb(63,81,181);-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,rgb(63,81,181)16px,rgb(63,81,181)0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:rgb(63,81,181);border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:rgb(63,81,181);border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:rgb(63,81,181);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:rgb(63,81,181);transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:rgb(63,81,181);transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgb(63,81,181)0%,rgb(63,81,181)37.5%,rgba(63,81,181,.26)37.5%,rgba(63,81,181,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:rgb(63,81,181);transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:rgb(63,81,181)}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:rgb(224,64,251);float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(63,81,181,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:rgb(63,81,181);left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:rgb(63,81,181)}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(63,81,181);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:rgb(63,81,181)}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield--expandable .mdl-button--icon{top:16px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:rgb(63,81,181);font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:rgb(63,81,181);bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}body{margin:0}.styleguide-demo h1{margin:48px 24px 0}.styleguide-demo h1:after{content:'';display:block;width:100%;border-bottom:1px solid rgba(0,0,0,.5);margin-top:24px}.styleguide-demo{opacity:0;transition:opacity .6s ease}.styleguide-masthead{height:256px;background:#212121;padding:115px 16px 0}.styleguide-container{position:relative;max-width:960px;width:100%}.styleguide-title{color:#fff;bottom:auto;position:relative;font-size:56px;font-weight:300;line-height:1;letter-spacing:-.02em}.styleguide-title:after{border-bottom:0}.styleguide-title span{font-weight:300}.mdl-styleguide .mdl-layout__drawer .mdl-navigation__link{padding:10px 24px}.demosLoaded .styleguide-demo{opacity:1}iframe{display:block;width:100%;border:none}iframe.heightSet{overflow:hidden}.demo-wrapper{margin:24px}.demo-wrapper iframe{border:1px solid rgba(0,0,0,.5)} \ No newline at end of file diff --git a/modules/material/www/material.orange-light_blue.1.2.1.min.css b/modules/material/www/material.orange-light_blue.1.2.1.min.css new file mode 100644 index 00000000..b45f502d --- /dev/null +++ b/modules/material/www/material.orange-light_blue.1.2.1.min.css @@ -0,0 +1,8 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.2.1 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:rgb(64,196,255);font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:rgb(255,152,0)!important}.mdl-color--primary-contrast{background-color:rgb(66,66,66)!important}.mdl-color--primary-dark{background-color:rgb(245,124,0)!important}.mdl-color--accent{background-color:rgb(64,196,255)!important}.mdl-color--accent-contrast{background-color:rgb(66,66,66)!important}.mdl-color-text--primary{color:rgb(255,152,0)!important}.mdl-color-text--primary-contrast{color:rgb(66,66,66)!important}.mdl-color-text--primary-dark{color:rgb(245,124,0)!important}.mdl-color-text--accent{color:rgb(64,196,255)!important}.mdl-color-text--accent-contrast{color:rgb(66,66,66)!important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:rgb(64,196,255);color:rgb(66,66,66)}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:rgb(64,196,255);background:rgba(66,66,66,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(255,152,0)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(255,152,0);color:rgb(66,66,66)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(255,152,0)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(255,152,0)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(255,152,0)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(66,66,66)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(64,196,255);color:rgb(66,66,66)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(64,196,255)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(64,196,255)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(64,196,255)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(66,66,66)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(255,152,0)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(66,66,66)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(66,66,66);background-color:rgb(255,152,0)}.mdl-button--accent.mdl-button--accent{color:rgb(64,196,255)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(66,66,66)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(66,66,66);background-color:rgb(64,196,255)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:rgb(64,196,255);background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(255,152,0)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(255,152,0,.26);background-color:rgba(255,152,0,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(255,152,0)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(255,152,0)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-chip{height:32px;font-family:"Roboto","Helvetica","Arial",sans-serif;line-height:32px;padding:0 12px;border:0;border-radius:16px;background-color:#dedede;display:inline-block;color:rgba(0,0,0,.87);margin:2px 0;font-size:0;white-space:nowrap}.mdl-chip__text{font-size:13px;vertical-align:middle;display:inline-block}.mdl-chip__action{height:24px;width:24px;background:0 0;opacity:.54;cursor:pointer;padding:0;margin:0 0 0 4px;font-size:13px;text-decoration:none;color:rgba(0,0,0,.87);border:none;outline:none}.mdl-chip__action,.mdl-chip__contact{display:inline-block;vertical-align:middle;overflow:hidden;text-align:center}.mdl-chip__contact{height:32px;width:32px;border-radius:16px;margin-right:8px;font-size:18px;line-height:32px}.mdl-chip:focus{outline:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-chip:active{background-color:#d6d6d6}.mdl-chip--deletable{padding-right:4px}.mdl-chip--contact{padding-left:0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:rgb(255,152,0)}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(255,152,0,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(255,152,0);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(66,66,66,.7),rgba(66,66,66,.7)),linear-gradient(to right,rgb(255,152,0),rgb(255,152,0));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(66,66,66,.7),rgba(66,66,66,.7)),linear-gradient(to right,rgb(255,152,0),rgb(255,152,0));-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(66,66,66,.9),rgba(66,66,66,.9)),linear-gradient(to right,rgb(255,152,0),rgb(255,152,0))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(255,152,0);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#e0e0e0;color:#000}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:56px;font-family:Helvetica,Arial,sans-serif;margin:8px 12px;top:0;left:0;color:rgb(66,66,66);z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:rgb(66,66,66);background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout__drawer-button{line-height:54px}.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button,.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:rgb(255,152,0);color:rgb(66,66,66);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:rgb(66,66,66);line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:rgb(255,152,0);overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:rgb(255,152,0);color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:rgb(66,66,66)}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(66,66,66,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:rgb(66,66,66)}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(64,196,255);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:rgb(66,66,66)}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid rgb(255,152,0)}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:rgb(255,152,0)}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:rgb(255,152,0)}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:rgb(255,152,0);-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,rgb(255,152,0)16px,rgb(255,152,0)0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:rgb(255,152,0);border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:rgb(255,152,0);border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(255,152,0,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(255,152,0,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:rgb(255,152,0);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:rgb(255,152,0);transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:rgb(255,152,0);transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgb(255,152,0)0%,rgb(255,152,0)37.5%,rgba(255,152,0,.26)37.5%,rgba(255,152,0,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:rgb(255,152,0);transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:rgb(255,152,0)}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:rgb(64,196,255);float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:rgb(255,152,0)}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:rgb(255,152,0)}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:rgb(255,152,0)}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:rgb(255,152,0)}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(255,152,0,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:rgb(255,152,0);left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(255,152,0,.26);background-color:rgba(255,152,0,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:rgb(255,152,0)}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(255,152,0);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:rgb(255,152,0)}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield--expandable .mdl-button--icon{top:16px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:rgb(255,152,0);font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:rgb(255,152,0);bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}body{margin:0}.styleguide-demo h1{margin:48px 24px 0}.styleguide-demo h1:after{content:'';display:block;width:100%;border-bottom:1px solid rgba(0,0,0,.5);margin-top:24px}.styleguide-demo{opacity:0;transition:opacity .6s ease}.styleguide-masthead{height:256px;background:#212121;padding:115px 16px 0}.styleguide-container{position:relative;max-width:960px;width:100%}.styleguide-title{color:#fff;bottom:auto;position:relative;font-size:56px;font-weight:300;line-height:1;letter-spacing:-.02em}.styleguide-title:after{border-bottom:0}.styleguide-title span{font-weight:300}.mdl-styleguide .mdl-layout__drawer .mdl-navigation__link{padding:10px 24px}.demosLoaded .styleguide-demo{opacity:1}iframe{display:block;width:100%;border:none}iframe.heightSet{overflow:hidden}.demo-wrapper{margin:24px}.demo-wrapper iframe{border:1px solid rgba(0,0,0,.5)} \ No newline at end of file diff --git a/modules/material/www/material.red-teal.1.2.1.min.css b/modules/material/www/material.red-teal.1.2.1.min.css new file mode 100644 index 00000000..02ae702a --- /dev/null +++ b/modules/material/www/material.red-teal.1.2.1.min.css @@ -0,0 +1,8 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.2.1 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:rgb(100,255,218);font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:rgb(244,67,54)!important}.mdl-color--primary-contrast{background-color:rgb(255,255,255)!important}.mdl-color--primary-dark{background-color:rgb(211,47,47)!important}.mdl-color--accent{background-color:rgb(100,255,218)!important}.mdl-color--accent-contrast{background-color:rgb(66,66,66)!important}.mdl-color-text--primary{color:rgb(244,67,54)!important}.mdl-color-text--primary-contrast{color:rgb(255,255,255)!important}.mdl-color-text--primary-dark{color:rgb(211,47,47)!important}.mdl-color-text--accent{color:rgb(100,255,218)!important}.mdl-color-text--accent-contrast{color:rgb(66,66,66)!important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:rgb(100,255,218);color:rgb(66,66,66)}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:rgb(100,255,218);background:rgba(66,66,66,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(244,67,54)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(244,67,54);color:rgb(255,255,255)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(244,67,54)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(244,67,54)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(244,67,54)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(100,255,218);color:rgb(66,66,66)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(100,255,218)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(100,255,218)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(100,255,218)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(66,66,66)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(244,67,54)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(255,255,255)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(244,67,54)}.mdl-button--accent.mdl-button--accent{color:rgb(100,255,218)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(66,66,66)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(66,66,66);background-color:rgb(100,255,218)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:rgb(100,255,218);background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(244,67,54)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(244,67,54,.26);background-color:rgba(244,67,54,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(244,67,54)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(244,67,54)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-chip{height:32px;font-family:"Roboto","Helvetica","Arial",sans-serif;line-height:32px;padding:0 12px;border:0;border-radius:16px;background-color:#dedede;display:inline-block;color:rgba(0,0,0,.87);margin:2px 0;font-size:0;white-space:nowrap}.mdl-chip__text{font-size:13px;vertical-align:middle;display:inline-block}.mdl-chip__action{height:24px;width:24px;background:0 0;opacity:.54;cursor:pointer;padding:0;margin:0 0 0 4px;font-size:13px;text-decoration:none;color:rgba(0,0,0,.87);border:none;outline:none}.mdl-chip__action,.mdl-chip__contact{display:inline-block;vertical-align:middle;overflow:hidden;text-align:center}.mdl-chip__contact{height:32px;width:32px;border-radius:16px;margin-right:8px;font-size:18px;line-height:32px}.mdl-chip:focus{outline:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-chip:active{background-color:#d6d6d6}.mdl-chip--deletable{padding-right:4px}.mdl-chip--contact{padding-left:0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:rgb(244,67,54)}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(244,67,54,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(244,67,54);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(244,67,54),rgb(244,67,54));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(244,67,54),rgb(244,67,54));-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,rgb(244,67,54),rgb(244,67,54))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(244,67,54);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#e0e0e0;color:#000}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:56px;font-family:Helvetica,Arial,sans-serif;margin:8px 12px;top:0;left:0;color:rgb(255,255,255);z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:rgb(255,255,255);background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout__drawer-button{line-height:54px}.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button,.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:rgb(244,67,54);color:rgb(255,255,255);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:rgb(255,255,255);line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:rgb(244,67,54);overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:rgb(244,67,54);color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:rgb(255,255,255)}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:rgb(255,255,255)}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(100,255,218);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:rgb(255,255,255)}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid rgb(244,67,54)}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:rgb(244,67,54)}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:rgb(244,67,54)}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:rgb(244,67,54);-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,rgb(244,67,54)16px,rgb(244,67,54)0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:rgb(244,67,54);border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:rgb(244,67,54);border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(244,67,54,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(244,67,54,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:rgb(244,67,54);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:rgb(244,67,54);transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:rgb(244,67,54);transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgb(244,67,54)0%,rgb(244,67,54)37.5%,rgba(244,67,54,.26)37.5%,rgba(244,67,54,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:rgb(244,67,54);transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:rgb(244,67,54)}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:rgb(100,255,218);float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:rgb(244,67,54)}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:rgb(244,67,54)}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:rgb(244,67,54)}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:rgb(244,67,54)}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(244,67,54,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:rgb(244,67,54);left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(244,67,54,.26);background-color:rgba(244,67,54,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:rgb(244,67,54)}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(244,67,54);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:rgb(244,67,54)}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield--expandable .mdl-button--icon{top:16px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:rgb(244,67,54);font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:rgb(244,67,54);bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}body{margin:0}.styleguide-demo h1{margin:48px 24px 0}.styleguide-demo h1:after{content:'';display:block;width:100%;border-bottom:1px solid rgba(0,0,0,.5);margin-top:24px}.styleguide-demo{opacity:0;transition:opacity .6s ease}.styleguide-masthead{height:256px;background:#212121;padding:115px 16px 0}.styleguide-container{position:relative;max-width:960px;width:100%}.styleguide-title{color:#fff;bottom:auto;position:relative;font-size:56px;font-weight:300;line-height:1;letter-spacing:-.02em}.styleguide-title:after{border-bottom:0}.styleguide-title span{font-weight:300}.mdl-styleguide .mdl-layout__drawer .mdl-navigation__link{padding:10px 24px}.demosLoaded .styleguide-demo{opacity:1}iframe{display:block;width:100%;border:none}iframe.heightSet{overflow:hidden}.demo-wrapper{margin:24px}.demo-wrapper iframe{border:1px solid rgba(0,0,0,.5)} \ No newline at end of file diff --git a/modules/material/www/material.teal-blue.1.2.1.min.css b/modules/material/www/material.teal-blue.1.2.1.min.css new file mode 100644 index 00000000..e0743668 --- /dev/null +++ b/modules/material/www/material.teal-blue.1.2.1.min.css @@ -0,0 +1,8 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.2.1 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:rgb(68,138,255);font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:rgb(0,150,136)!important}.mdl-color--primary-contrast{background-color:rgb(255,255,255)!important}.mdl-color--primary-dark{background-color:rgb(0,121,107)!important}.mdl-color--accent{background-color:rgb(68,138,255)!important}.mdl-color--accent-contrast{background-color:rgb(255,255,255)!important}.mdl-color-text--primary{color:rgb(0,150,136)!important}.mdl-color-text--primary-contrast{color:rgb(255,255,255)!important}.mdl-color-text--primary-dark{color:rgb(0,121,107)!important}.mdl-color-text--accent{color:rgb(68,138,255)!important}.mdl-color-text--accent-contrast{color:rgb(255,255,255)!important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:rgb(68,138,255);color:rgb(255,255,255)}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:rgb(68,138,255);background:rgba(255,255,255,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(0,150,136)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(0,150,136);color:rgb(255,255,255)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(0,150,136)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(0,150,136)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(0,150,136)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(68,138,255);color:rgb(255,255,255)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(68,138,255)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(68,138,255)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(68,138,255)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(0,150,136)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(255,255,255)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(0,150,136)}.mdl-button--accent.mdl-button--accent{color:rgb(68,138,255)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(255,255,255)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(68,138,255)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:rgb(68,138,255);background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(0,150,136)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,150,136,.26);background-color:rgba(0,150,136,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(0,150,136)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(0,150,136)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-chip{height:32px;font-family:"Roboto","Helvetica","Arial",sans-serif;line-height:32px;padding:0 12px;border:0;border-radius:16px;background-color:#dedede;display:inline-block;color:rgba(0,0,0,.87);margin:2px 0;font-size:0;white-space:nowrap}.mdl-chip__text{font-size:13px;vertical-align:middle;display:inline-block}.mdl-chip__action{height:24px;width:24px;background:0 0;opacity:.54;cursor:pointer;padding:0;margin:0 0 0 4px;font-size:13px;text-decoration:none;color:rgba(0,0,0,.87);border:none;outline:none}.mdl-chip__action,.mdl-chip__contact{display:inline-block;vertical-align:middle;overflow:hidden;text-align:center}.mdl-chip__contact{height:32px;width:32px;border-radius:16px;margin-right:8px;font-size:18px;line-height:32px}.mdl-chip:focus{outline:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-chip:active{background-color:#d6d6d6}.mdl-chip--deletable{padding-right:4px}.mdl-chip--contact{padding-left:0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:rgb(0,150,136)}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(0,150,136,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(0,150,136);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(0,150,136),rgb(0,150,136));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(0,150,136),rgb(0,150,136));-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,rgb(0,150,136),rgb(0,150,136))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(0,150,136);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#e0e0e0;color:#000}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:56px;font-family:Helvetica,Arial,sans-serif;margin:8px 12px;top:0;left:0;color:rgb(255,255,255);z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:rgb(255,255,255);background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout__drawer-button{line-height:54px}.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button,.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:rgb(0,150,136);color:rgb(255,255,255);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:rgb(255,255,255);line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:rgb(0,150,136);overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:rgb(0,150,136);color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:rgb(255,255,255)}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:rgb(255,255,255)}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(68,138,255);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:rgb(255,255,255)}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid rgb(0,150,136)}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:rgb(0,150,136)}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:rgb(0,150,136)}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:rgb(0,150,136);-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,rgb(0,150,136)16px,rgb(0,150,136)0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:rgb(0,150,136);border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:rgb(0,150,136);border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,150,136,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,150,136,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:rgb(0,150,136);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:rgb(0,150,136);transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:rgb(0,150,136);transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgb(0,150,136)0%,rgb(0,150,136)37.5%,rgba(0,150,136,.26)37.5%,rgba(0,150,136,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:rgb(0,150,136);transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:rgb(0,150,136)}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:rgb(68,138,255);float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:rgb(0,150,136)}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:rgb(0,150,136)}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:rgb(0,150,136)}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:rgb(0,150,136)}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(0,150,136,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:rgb(0,150,136);left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,150,136,.26);background-color:rgba(0,150,136,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:rgb(0,150,136)}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(0,150,136);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:rgb(0,150,136)}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield--expandable .mdl-button--icon{top:16px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:rgb(0,150,136);font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:rgb(0,150,136);bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}body{margin:0}.styleguide-demo h1{margin:48px 24px 0}.styleguide-demo h1:after{content:'';display:block;width:100%;border-bottom:1px solid rgba(0,0,0,.5);margin-top:24px}.styleguide-demo{opacity:0;transition:opacity .6s ease}.styleguide-masthead{height:256px;background:#212121;padding:115px 16px 0}.styleguide-container{position:relative;max-width:960px;width:100%}.styleguide-title{color:#fff;bottom:auto;position:relative;font-size:56px;font-weight:300;line-height:1;letter-spacing:-.02em}.styleguide-title:after{border-bottom:0}.styleguide-title span{font-weight:300}.mdl-styleguide .mdl-layout__drawer .mdl-navigation__link{padding:10px 24px}.demosLoaded .styleguide-demo{opacity:1}iframe{display:block;width:100%;border:none}iframe.heightSet{overflow:hidden}.demo-wrapper{margin:24px}.demo-wrapper iframe{border:1px solid rgba(0,0,0,.5)} \ No newline at end of file diff --git a/modules/material/www/mfa-backupcode.svg b/modules/material/www/mfa-backupcode.svg new file mode 100644 index 00000000..4a23bd31 --- /dev/null +++ b/modules/material/www/mfa-backupcode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/material/www/mfa-manager.svg b/modules/material/www/mfa-manager.svg new file mode 100644 index 00000000..a4cd8c07 --- /dev/null +++ b/modules/material/www/mfa-manager.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/material/www/mfa-totp.svg b/modules/material/www/mfa-totp.svg new file mode 100644 index 00000000..1af9af86 --- /dev/null +++ b/modules/material/www/mfa-totp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/modules/material/www/mfa-u2f-api.js b/modules/material/www/mfa-u2f-api.js new file mode 100644 index 00000000..a0518ef0 --- /dev/null +++ b/modules/material/www/mfa-u2f-api.js @@ -0,0 +1,748 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. +u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); + }; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; diff --git a/modules/material/www/mfa-u2f.svg b/modules/material/www/mfa-u2f.svg new file mode 100644 index 00000000..3f1a677f --- /dev/null +++ b/modules/material/www/mfa-u2f.svg @@ -0,0 +1,27 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + + diff --git a/modules/material/www/mfa-webauthn.svg b/modules/material/www/mfa-webauthn.svg new file mode 100644 index 00000000..3f1a677f --- /dev/null +++ b/modules/material/www/mfa-webauthn.svg @@ -0,0 +1,27 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + + diff --git a/modules/material/www/shield.svg b/modules/material/www/shield.svg new file mode 100644 index 00000000..1de5a003 --- /dev/null +++ b/modules/material/www/shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/modules/material/www/simplewebauthn/LICENSE.md b/modules/material/www/simplewebauthn/LICENSE.md new file mode 100644 index 00000000..70730ac2 --- /dev/null +++ b/modules/material/www/simplewebauthn/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Matthew Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/modules/material/www/simplewebauthn/browser.js b/modules/material/www/simplewebauthn/browser.js new file mode 100644 index 00000000..8b1de96e --- /dev/null +++ b/modules/material/www/simplewebauthn/browser.js @@ -0,0 +1,2 @@ +/* [@simplewebauthn/browser] Version: 4.1.0 - Wednesday, September 1st, 2021, 9:11:50 AM */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).SimpleWebAuthnBrowser={})}(this,(function(e){"use strict";function t(e){const t=new Uint8Array(e);let n="";for(const e of t)n+=String.fromCharCode(e);return btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function n(e){const t=e.replace(/-/g,"+").replace(/_/g,"/"),n=(4-t.length%4)%4,r=t.padEnd(t.length+n,"="),o=atob(r),i=new ArrayBuffer(o.length),a=new Uint8Array(i);for(let e=0;e i { + margin: 0 1em; +} + +i.material-icons.mdl-typography--display-4 { + font-size: 112px; /* needed to override font-size established in material icons. */ +} + +.margin { + margin: 1em; +} + +.white-bg { + background-color: white; +} + +/* file entire card, so need to match mdl-card's min-height of 200px */ +.fixed-height { + height: 200px; +} + +.fill-parent { + width: 100%; + height: 100%; +} + +.scale-to-parent { + max-width: 90%; + max-height: 90%; +} + +.hide { + visibility: hidden; +} + +.show { + visibility: visible; +} + +/* out-of-box styles for the cards don't work well when there are many cards */ +.mdl-card { + width: initial; + margin: 1em; +} + +/* cards on profile review needed to have consistent width */ +.mdl-card.fixed-width { + width: 365px; /* takes into acocunt "... (10 remaining)" for appropriate width */ +} + +/* don't want images to be too small on cards */ +.mdl-card__media > img { + min-width: 20%; +} + +/* phones (2 cards / row) */ +@media only screen and (min-width : 320px) { + .mdl-card.row-aware { + min-width: calc(50% - 2em); + max-width: calc(50% - 2em); + } + + .mdl-card.fill-phone-viewport { + height: 100vh; + width: 100vw; + } +} +/* phones, small tablets, landscape */ +@media only screen and (min-width : 600px) { + /* (3 cards / row) */ + .mdl-card.row-aware { + min-width: calc(33% - 2em); + max-width: calc(33% - 2em); + } + + .mdl-card.fill-phone-viewport { + height: auto; + width: auto; + } +} +/* tablets, small desktops */ +@media only screen and (min-width : 850px) { + /* (4 cards / row) */ + .mdl-card.row-aware { + min-width: calc(25% - 2em); + max-width: calc(25% - 2em); + } + + .mdl-card.fill-phone-viewport { + height: auto; + width: auto; + } +} +/* desktops */ +@media only screen and (min-width : 1024px) { + /* (5 cards / row) */ + .mdl-card.row-aware { + min-width: calc(20% - 2em); + max-width: calc(20% - 2em); + } + + .mdl-card.fill-phone-viewport { + height: auto; + width: auto; + } +} + +.mdl-card.disabled, .mdl-button[disabled].not-allowed, a[href=''][download].mdl-button--disabled.not-allowed { + cursor: not-allowed; +} + +.mdl-card.disabled img { + opacity: 0.3; +} + +.alert { + max-width: 80%; + min-width: 30%; + background-color: tomato; + border-radius: 0.33em; + padding: 1em; + + /* had to center manually instead of using flex due to ie11 bug that + was causing idp cards not to wrap when parent container was centered + */ + margin-left: auto; + margin-right: auto; +} + +.alert a { + color: blue; +} + +/* The font-size in the mdl-textfield was overriding the one in caption since + it was defined later in the CSS but the font-size from caption is what was + needed here so more specificity required to override it back */ +.mdl-textfield.mdl-typography--caption { + font-size: 12px +} + + +/* special case where we want a button for all it's built-in characteristics, +e.g., primary color, but also want to set the text apart a bit. */ +.mdl-button.mdl-typography--caption { + text-transform: none; + font-size: 12px +} +a.mdl-button.mdl-typography--body-2, a.mdl-button.mdl-typography--body-2 > i.material-icons { + text-transform: none; + font-size: 14px +} + +/* didn't feel like the padding was enough out of the box */ +.mdl-card__actions { + padding: 1em; +} + +/* needed ability to center text in these card titles at times */ +.mdl-card__title.center { + justify-content: center; +} + +[flex] { + flex: 1; +} + +.gradient-bg { + background: linear-gradient(rgba(200,200,200,1) 0%, + rgba(250,250,250,1) 50%, + rgba(200,200,200,1) 100%); +} + +.mdl-card > .mdl-card__media > img.icon { + max-width: 24px; +} + +/* Material icons */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(material-icons.woff2) format('woff2'), + url(material-icons.woff) format('woff'); +} From c87a2551a79a5efda5c4a5255f21bd806c564571 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 21 May 2024 18:07:00 +0800 Subject: [PATCH 63/92] corrections to manual test instructions --- Makefile | 7 +------ docker-compose.yml | 7 +++++++ docs/material_tests.md | 46 +++++++++++++++++++++--------------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index 0ae85386..06fdce1f 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,5 @@ -start: ssp - -ssp: clean - docker-compose up -d ssp - hub: clean - docker-compose up -d ssp-hub.local ssp-sp1.local sp2 ssp-idp1.local idp2 + docker-compose up -d ssp-hub.local clean: docker-compose kill diff --git a/docker-compose.yml b/docker-compose.yml index 90b6140a..4223a68f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -106,6 +106,13 @@ services: ssp-hub.local: build: . + depends_on: + - ssp-idp1.local + - ssp-idp2.local + - ssp-idp3.local + - ssp-sp1.local + - ssp-sp2.local + - ssp-sp3.local volumes: # Utilize custom certs - ./development/hub/cert:/data/vendor/simplesamlphp/simplesamlphp/cert diff --git a/docs/material_tests.md b/docs/material_tests.md index fef075af..39e5c7b1 100644 --- a/docs/material_tests.md +++ b/docs/material_tests.md @@ -6,23 +6,23 @@ ## Setup -1. Setup `localhost` (or `192.168.62.54`, if using Vagrant) aliases for `ssp-hub1.local`, `ssp-hub2.local`, `ssp-idp1.local`, `ssp-idp2.local`, `ssp-idp3.local`, `ssp-idp4.local`, `ssp-sp1.local` and `ssp-sp2.local`. This is typically done in `/etc/hosts`. _Example line: `0.0.0.0 ssp-hub1.local ssp-idp1.local ssp-idp2.local ssp-idp4.local ssp-hub2.local ssp-idp3.local ssp-sp1.local ssp-sp2.local`_ +1. Setup `localhost` (or `192.168.62.54`, if using Vagrant) aliases for `ssp-hub.local`, `ssp-hub2.local`, `ssp-idp1.local`, `ssp-idp2.local`, `ssp-idp3.local`, `ssp-idp4.local`, `ssp-sp1.local` and `ssp-sp2.local`. This is typically done in `/etc/hosts`. _Example line: `0.0.0.0 ssp-hub.local ssp-idp1.local ssp-idp2.local ssp-idp4.local ssp-hub2.local ssp-idp3.local ssp-sp1.local ssp-sp2.local`_ 1. Start test environment, i.e., `make` from the command line. ## Hub page -1. Goto [Hub 1](http://ssp-hub1.local/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [Hub 1](http://ssp-hub.local/module.php/core/authenticate.php?as=hub-discovery) ## Error page -1. Goto [Hub 1](http://ssp-hub1.local) +1. Goto [Hub 1](http://ssp-hub.local) 1. Click **Federation** tab 1. Click either **Show metadata** link 1. Login as hub administrator: `username=`**admin** `password=`**abc123** ## Logout page -1. Goto [Hub 1](http://ssp-hub1.local) +1. Goto [Hub 1](http://ssp-hub.local) 1. Click **Authentication** tab 1. Click **Test configured authentication sources** 1. Click **admin** @@ -33,25 +33,25 @@ ### Without theme in place -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp1** (first one) 1. login page should **NOT** have material design ### With theme in place -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp2** (second one) 1. login page **SHOULD** have material design ## Forgot password functionality -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp2** (second one) 1. Forgot password link should be visible ## Helpful links functionality -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Help link should be visible under login form 1. Profile link should be visible under login form @@ -62,7 +62,7 @@ _Note: This nag only works once since choosing later will simply set the nag date into the future a little._ -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp2** (second one) 1. Login as an "about to expire" user: `username=`**near_future** `password=`**a** 1. Click **Later** @@ -70,7 +70,7 @@ _Note: This nag only works once since choosing later will simply set the nag da ### Expired page -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp2** (second one) 1. Login as an "expired" user: `username=`**already_past** `password=`**a** @@ -78,7 +78,7 @@ _Note: This nag only works once since choosing later will simply set the nag da ### Nag about missing MFA setup -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as an "unprotected" user: `username=`**nag_for_mfa** `password=`**a** 1. The "learn more" link should be visible @@ -89,7 +89,7 @@ _Note: This nag only works once since choosing later will simply set the nag da ### Nag about missing password recovery methods -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a user without any methods: `username=`**nag_for_method** `password=`**a** 1. Enter one of the following codes to verify (`94923279, 82743523, 77802769, 01970541, 37771076`) @@ -100,13 +100,13 @@ _Note: This nag only works once since choosing later will simply set the nag da ### Force MFA setup -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as an "unsafe" user: `username=`**must_set_up_mfa** `password=`**a** ### Backup code -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "backup code" user: `username=`**has_backupcode** `password=`**a** 1. Enter one of the following codes to verify (`94923279, 82743523, 77802769, 01970541, 37771076`) @@ -116,7 +116,7 @@ _Note: This nag only works once since choosing later will simply set the nag da ### TOTP code -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "totp" user: `username=`**has_totp** `password=`**a** 1. Set up an app using this secret, `JVRXKYTMPBEVKXLS` @@ -125,7 +125,7 @@ _Note: This nag only works once since choosing later will simply set the nag da ### Key (U2F) -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "u2f" user: `username=`**has_u2f** `password=`**a** 1. Insert key and press @@ -133,7 +133,7 @@ _Note: This nag only works once since choosing later will simply set the nag da ### Key (WebAuthn) -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "webauthn" user: `username=`**has_webauthn** `password=`**a** 1. Insert key and press @@ -141,21 +141,21 @@ _Note: This nag only works once since choosing later will simply set the nag da ### Multiple options -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "multiple option" user: `username=`**has_all** `password=`**a** 1. Click **MORE OPTIONS** ### Multiple options (legacy, with U2F) -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "multiple option" user: `username=`**has_all_legacy** `password=`**a** 1. Click **MORE OPTIONS** ### Manager rescue -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "multiple option" user: `username=`**has_all** `password=`**a** 1. Click **MORE OPTIONS** @@ -166,18 +166,18 @@ _NOTE: At this time, the correct code is not known and can't be tested locally ( ## Announcements functionality -1. Goto [SP 2](http://ssp-sp2.local:8083/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 2](http://ssp-sp2.local:8082/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. The announcement should be displayed on the hub 1. Click **idp3** (first one) 1. The announcement should be displayed at the login screen ## SP name functionality -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. The sp name should appear in the banner ## Profile review functionality -1. Goto [SP 1](http://ssp-sp1.local:8082/module.php/core/authenticate.php?as=hub-discovery) +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "Review needed" user: `username=`**needs_review** `password=`**a** 1. Enter one of the following printable codes to verify (`94923279, 82743523, 77802769, 01970541, 37771076`) From 3fc98ded6e4c3f31f7300007ea07e0e75f7a0290 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 10:55:38 +0800 Subject: [PATCH 64/92] swap ssp-sp3.local and pwmanager.local ports was: ssp-sp3.local:8084 and pwmanager.local:8083 now: ssp-sp3.local:8083 and pwmanager.local:8084 The reason is twofold: (1) this makes the ssp-sp[n].local container ports contiguous: 8081, 8082, and 8083, and (2) some of the references to ssp-sp3.local still had 8083 as was assigned in the simplesamlphp-module-sildisco repo. --- actions-services.yml | 4 ++-- development/sp-local/config/authsources-pwmanager.php | 2 +- docker-compose.yml | 8 ++++---- docs/development.md | 7 ++++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/actions-services.yml b/actions-services.yml index 9c3cef6d..e3b6b5df 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -111,9 +111,9 @@ services: ID_BROKER_ASSERT_VALID_IP: "false" ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" - MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub-custom-port" REMEMBER_ME_SECRET: "12345" - PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub-custom-port" PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" diff --git a/development/sp-local/config/authsources-pwmanager.php b/development/sp-local/config/authsources-pwmanager.php index ea9c8ab0..80aee9c9 100644 --- a/development/sp-local/config/authsources-pwmanager.php +++ b/development/sp-local/config/authsources-pwmanager.php @@ -12,7 +12,7 @@ 'mfa-idp' => [ 'saml:SP', - 'entityID' => 'http://pwmanager.local:8083', + 'entityID' => 'http://pwmanager.local:8084', 'idp' => 'http://ssp-idp1.local:8085', 'discoURL' => null, 'NameIDPolicy' => "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", diff --git a/docker-compose.yml b/docker-compose.yml index 90b6140a..858ed6fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -190,9 +190,9 @@ services: ID_BROKER_ASSERT_VALID_IP: "false" ID_BROKER_BASE_URI: "dummy" ID_BROKER_TRUSTED_IP_RANGES: "192.168.0.1/8" - MFA_SETUP_URL: "http://pwmanager.local:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + MFA_SETUP_URL: "http://pwmanager.local:8084/module.php/core/authenticate.php?as=ssp-hub-custom-port" REMEMBER_ME_SECRET: "12345" - PROFILE_URL: "http://pwmanager:8083/module.php/core/authenticate.php?as=ssp-hub-custom-port" + PROFILE_URL: "http://pwmanager.local:8084/module.php/core/authenticate.php?as=ssp-hub-custom-port" PROFILE_URL_FOR_TESTS: "http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub" SECURE_COOKIE: "false" SHOW_SAML_ERRORS: "true" @@ -342,7 +342,7 @@ services: # Utilize custom metadata - ./development/sp3-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php ports: - - "8084:80" + - "8083:80" env_file: - local.env environment: @@ -366,7 +366,7 @@ services: # Utilize custom metadata - ./development/sp-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php ports: - - "8083:80" + - "8084:80" environment: - ADMIN_EMAIL=john_doe@there.com - ADMIN_PASS=sp1 diff --git a/docs/development.md b/docs/development.md index bff583dc..e7f047c9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,7 +1,8 @@ -Three SPs, a hub (a combined IdP and SP) and three IdPs get spun up by docker-compose. In order for this to work, you will need to edit your hosts file to include entries for the following domains ... +Four SPs, a hub (a combined IdP and SP) and three IdPs get spun up by docker-compose. In order for this to work, you will need to edit your hosts file to include entries for the following domains ... * ssp-sp1.local # to be used with port 8081 -* ssp-sp2.local # to be used with port 8082 -* ssp-sp3.local # to be used with port 8084 +* ssp-sp2.local # to be used with port 8082 +* ssp-sp3.local # to be used with port 8083 +* pwmanager.local # to be used with port 8084 * ssp-hub.local * ssp-idp1.local # to be used with port 8085 * ssp-idp2.local # to be used with port 8086 From 1b1fc7bcb3d2f90669f64a41f0572cf4a89b90cd Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 10:57:23 +0800 Subject: [PATCH 65/92] remove unused "ssp" container --- docker-compose.yml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4223a68f..1f47bfe9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,4 @@ -version: "2" services: - ssp: - build: . - volumes: - # Utilize custom certs - - ./development/ssp/cert:/data/vendor/simplesamlphp/simplesamlphp/cert - - # Utilize custom configs - - ./development/ssp/config/authsources.php:/data/vendor/simplesamlphp/simplesamlphp/config/authsources.php - - # Configure the debugger - - ./development/ssp/run-debug.sh:/data/run-debug.sh - - # Local modules - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - command: ["/data/run-debug.sh"] - ports: - - "80:80" - env_file: - - ./local.env - db: image: mariadb:10 ports: From 4b56e144c56bda4999a0f857a63902f7648fb7c2 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 11:04:57 +0800 Subject: [PATCH 66/92] fixes identified during PR review - add the port number to ssp-sp3.local in the authsources.php config - remove reference to the "default-enable" file in the_hub.md --- development/sp3-local/config/authsources.php | 2 +- docs/the_hub.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/development/sp3-local/config/authsources.php b/development/sp3-local/config/authsources.php index ebc96e6c..4559d445 100644 --- a/development/sp3-local/config/authsources.php +++ b/development/sp3-local/config/authsources.php @@ -37,7 +37,7 @@ // The entity ID of this SP. // Can be NULL/unset, in which case an entity ID is generated based on the metadata URL. - 'entityID' => 'http://ssp-sp3.local', + 'entityID' => 'http://ssp-sp3.local:8083', // The entity ID of the IdP this should SP should contact. // Can be NULL/unset, in which case the user will be shown a list of available IdPs. diff --git a/docs/the_hub.md b/docs/the_hub.md index d1c4b588..266826f7 100644 --- a/docs/the_hub.md +++ b/docs/the_hub.md @@ -1,7 +1,6 @@ The hub will need its certs, `config.php` and `authsources.php` files as a normal simplesamlphp installation. Examples of these can be found in the `./development/hub` folder. (Note the `discoURL` entry in the `authsources.php` file.) Other files it will need are as follows ... -* `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/default-enable` needs to be created (just an empty file) * The files in the `./lib` folder will need to go into `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/lib` * The files in the `./www` folder will need to go into `/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/www` * The `./sspoverrides/www_saml2_idp/SSOService.php` file will need overwrite the same out-of-the-box file in `/data/vendor/simplesamlphp/simplesamlphp/www/saml2/idp/` From 488245694b0740e5203ce9145f23065daa3a4b1b Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 11:23:24 +0800 Subject: [PATCH 67/92] add Make target to update simplewebauthn/browser dependency --- .gitignore | 3 ++- Makefile | 12 ++++++++++++ docker-compose.yml | 8 ++++++++ package.json | 6 ++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 2b9d4ce1..9f34c408 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ local.env .vagrant composer.phar /nbproject/ -local.*.env +*.env *.aes dockercfg +node_modules/ diff --git a/Makefile b/Makefile index 06fdce1f..6fd4442f 100644 --- a/Makefile +++ b/Makefile @@ -16,3 +16,15 @@ test: test-integration: docker-compose run --rm test ./run-integration-tests.sh + +copyJsLib: + cp ./node_modules/@simplewebauthn/browser/dist/bundle/index.umd.min.js ./modules/material/www/simplewebauthn/browser.js + cp ./node_modules/@simplewebauthn/browser/LICENSE.md ./www/simplewebauthn/LICENSE.md + +deps: + docker-compose run --rm node npm install --ignore-scripts + make copyJsLib + +depsupdate: + docker-compose run --rm node npm update --ignore-scripts + make copyJsLib diff --git a/docker-compose.yml b/docker-compose.yml index eecb64eb..b4d7ec04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -420,6 +420,14 @@ services: - AWS_DEFAULT_REGION=us-east-1 - AWS_DYNAMODB_ENDPOINT=http://dynamo:8000 + node: + image: node:lts-alpine + volumes: + - ./package.json:/data/package.json + - ./package-lock.json:/data/package-lock.json + - ./node_modules:/data/node_modules + working_dir: /data + networks: default: driver: bridge diff --git a/package.json b/package.json new file mode 100644 index 00000000..aca8b163 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "simplesamlphp-module-material", + "dependencies": { + "@simplewebauthn/browser": "^4.1.0" + } +} From 3366b816d764afe179dc36a162c5f12e3de84c9a Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 13:24:56 +0800 Subject: [PATCH 68/92] use the new path to the yii script in run-idp.sh --- dockerbuild/run-idp.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dockerbuild/run-idp.sh b/dockerbuild/run-idp.sh index 11490e6d..f2faa512 100755 --- a/dockerbuild/run-idp.sh +++ b/dockerbuild/run-idp.sh @@ -2,9 +2,9 @@ # Try to run database migrations cd /data/vendor/simplesamlphp/simplesamlphp/modules/silauth -chmod a+x ./src/yii +chmod a+x ./lib/Auth/Source/yii -output=$(./src/yii migrate --interactive=0 2>&1) +output=$(./lib/Auth/Source/yii migrate --interactive=0 2>&1) # If they failed, exit. rc=$?; @@ -14,4 +14,4 @@ if [[ $rc != 0 ]]; then fi cd /data -./run.sh \ No newline at end of file +./run.sh From bafd40bdb81facde921f157a0f36c7ba2a397ff2 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 17:08:20 +0800 Subject: [PATCH 69/92] include a default authsources.php file --- Dockerfile | 2 +- dockerbuild/config/authsources.php | 17 +++++++++++++++++ .../{ssp-overrides => config}/config.php | 0 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 dockerbuild/config/authsources.php rename dockerbuild/{ssp-overrides => config}/config.php (100%) diff --git a/Dockerfile b/Dockerfile index 1c701fdb..9bc33440 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ RUN mv $SSP_PATH/www/saml2/idp/SingleLogoutService.php $SSP_PATH/www/saml2/idp/s COPY dockerbuild/ssp-overrides/SingleLogoutService.php $SSP_PATH/www/saml2/idp/SingleLogoutService.php COPY dockerbuild/ssp-overrides/saml20-idp-remote.php $SSP_PATH/metadata/saml20-idp-remote.php COPY dockerbuild/ssp-overrides/saml20-sp-remote.php $SSP_PATH/metadata/saml20-sp-remote.php -COPY dockerbuild/ssp-overrides/config.php $SSP_PATH/config/config.php +COPY dockerbuild/config/* $SSP_PATH/config/ COPY dockerbuild/ssp-overrides/id.php $SSP_PATH/www/id.php COPY dockerbuild/ssp-overrides/announcement.php $SSP_PATH/announcement/announcement.php COPY tests /data/tests diff --git a/dockerbuild/config/authsources.php b/dockerbuild/config/authsources.php new file mode 100644 index 00000000..21dc6df7 --- /dev/null +++ b/dockerbuild/config/authsources.php @@ -0,0 +1,17 @@ + [ + // The default is to use core:AdminPassword, but it can be replaced with + // any authentication source. + + 'core:AdminPassword', + ], + + // Use SilAuth + 'silauth' => ConfigManager::getSspConfig(), +]; diff --git a/dockerbuild/ssp-overrides/config.php b/dockerbuild/config/config.php similarity index 100% rename from dockerbuild/ssp-overrides/config.php rename to dockerbuild/config/config.php From 9438f24208527407a9ae9c355fde09eb41df947a Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 17:12:52 +0800 Subject: [PATCH 70/92] include sildisco in the volume mapping in docker-compose.yml This makes local development more convenient so you don't have to rebuild the image every time you make a change. But we don't want this in actions-services.yml since it's meant to test the final image. --- docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 858ed6fb..6d5b23e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: ["/data/run-debug.sh"] ports: @@ -81,6 +82,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: ["/data/run-tests.sh"] test-browser: @@ -130,6 +132,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: /data/run-debug.sh ports: - "80:80" @@ -175,6 +178,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco command: > bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && /data/run.sh" @@ -226,6 +230,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco ports: - "8086:80" environment: @@ -256,6 +261,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco ports: - "8087:80" env_file: @@ -289,6 +295,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco ports: - "8081:80" environment: @@ -318,6 +325,7 @@ services: - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco ports: - "8082:80" environment: From 4bf8403e36b82e397d03e67f421a8cab7d7bec87 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 22 May 2024 17:16:54 +0800 Subject: [PATCH 71/92] map all modules now that this is the last one to bring in to ssp-base --- docker-compose.yml | 42 +++++++----------------------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2b469b68..ddc44d13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,11 +52,7 @@ services: - ./features:/data/features - ./behat.yml:/data/behat.yml - ./tests:/data/tests - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules command: ["/data/run-tests.sh"] test-browser: @@ -109,11 +105,7 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules command: /data/run-debug.sh ports: - "80:80" @@ -155,11 +147,7 @@ services: - ./features:/data/features # Local modules - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules command: > bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && /data/run.sh" @@ -207,11 +195,7 @@ services: - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php # Local modules - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules ports: - "8086:80" environment: @@ -238,11 +222,7 @@ services: - ./development/idp3-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php # Local modules - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules ports: - "8087:80" env_file: @@ -272,11 +252,7 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules ports: - "8081:80" environment: @@ -302,11 +278,7 @@ services: - ./development/sp2-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php # Local modules - - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa - - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker - - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview - - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth - - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules ports: - "8082:80" environment: From c0caed4c29b86d3e65301e9f43180f73e901adb1 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Thu, 23 May 2024 14:43:57 +0800 Subject: [PATCH 72/92] add to the instructions for setting up a local dev environment [skip ci] --- README.md | 10 ++++++---- docs/material_tests.md | 6 +----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c94666ca..f87a0dd4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ must be installed. [Make](https://www.gnu.org/software/make) is optional but simplifies the build process. -[Vagrant](https://www.vagrantup.com) for Windows users. +[PHP](https://www.php.net) and [Composer](https://getcomposer.org) are optional, but at a minimum you need COMPOSER_CACHE_DIR set to a local directory for storing the PHP dependency cache. This must be set in your local development environment, not in the Docker container environment. For example, in your `~/.bashrc`, include `export COMPOSER_CACHE_DIR="$HOME/.composer"` and create an empty directory at `~/.composer`. ## Configuration By default, configuration is read from environment variables. These are documented @@ -34,9 +34,11 @@ will overwrite variables set in the execution environment. ## Local testing 1. `cp local.env.dist local.env` within project root and make adjustments as needed. -2. Add your github token to the `COMPOSER_AUTH` variable in the `local.env` file. -3. `make` or `docker-compose up -d` within the project root. -4. Visit http://localhost to see SSP running +2. `cp local.broker.env.dist local.broker.env` within project root and make adjustments as needed. +3. Add your github token to the `COMPOSER_AUTH` variable in the `local.env` file. +4. Create `localhost` aliases for `ssp-hub.local`, `ssp-idp1.local`, `ssp-idp2.local`, `ssp-idp3.local`, `ssp-sp1.local`, `ssp-sp2.local`, and `ssp-sp3.local`. This is typically done in `/etc/hosts`. _Example line: `127.0.0.1 ssp-hub.local ssp-idp1.local ssp-idp2.local ssp-idp3.local ssp-sp1.local ssp-sp2.local ssp-sp3.local`_ +4. `make` or `docker-compose up -d` within the project root. +5. Visit http://ssp-hub.local to see SimpleSAMLphp ### Setup PhpStorm for remote debugging with Docker diff --git a/docs/material_tests.md b/docs/material_tests.md index 39e5c7b1..a2cc2247 100644 --- a/docs/material_tests.md +++ b/docs/material_tests.md @@ -1,13 +1,9 @@ # Testing the Material Module theme -[Make](https://www.gnu.org/software/make/), [Docker](https://www.docker.com/products/overview) and -[Docker Compose](https://docs.docker.com/compose/install/) are required. - ## Setup -1. Setup `localhost` (or `192.168.62.54`, if using Vagrant) aliases for `ssp-hub.local`, `ssp-hub2.local`, `ssp-idp1.local`, `ssp-idp2.local`, `ssp-idp3.local`, `ssp-idp4.local`, `ssp-sp1.local` and `ssp-sp2.local`. This is typically done in `/etc/hosts`. _Example line: `0.0.0.0 ssp-hub.local ssp-idp1.local ssp-idp2.local ssp-idp4.local ssp-hub2.local ssp-idp3.local ssp-sp1.local ssp-sp2.local`_ -1. Start test environment, i.e., `make` from the command line. +See [Local Testing](../README.md#local-testing) for instructions to set up your local development environment. ## Hub page From f93cebfdc2e98309e1d4df0fff8d6591028fde15 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 24 May 2024 11:51:09 +0800 Subject: [PATCH 73/92] map each module individually in docker-compose the mapping of the entire module directory clobbered standard modules, so that's no good --- docker-compose.yml | 49 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ddc44d13..eb4939c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,7 +52,12 @@ services: - ./features:/data/features - ./behat.yml:/data/behat.yml - ./tests:/data/tests - - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules/material:/data/vendor/simplesamlphp/simplesamlphp/modules/material command: ["/data/run-tests.sh"] test-browser: @@ -105,7 +110,12 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules - - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules/material:/data/vendor/simplesamlphp/simplesamlphp/modules/material command: /data/run-debug.sh ports: - "80:80" @@ -147,7 +157,12 @@ services: - ./features:/data/features # Local modules - - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules/material:/data/vendor/simplesamlphp/simplesamlphp/modules/material command: > bash -c "whenavail db 3306 60 /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source/yii migrate --interactive=0 && /data/run.sh" @@ -195,7 +210,12 @@ services: - ./development/UserPass.php:/data/vendor/simplesamlphp/simplesamlphp/modules/exampleauth/lib/Auth/Source/UserPass.php # Local modules - - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules/material:/data/vendor/simplesamlphp/simplesamlphp/modules/material ports: - "8086:80" environment: @@ -222,7 +242,12 @@ services: - ./development/idp3-local/metadata/saml20-sp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-sp-remote.php # Local modules - - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules/material:/data/vendor/simplesamlphp/simplesamlphp/modules/material ports: - "8087:80" env_file: @@ -252,7 +277,12 @@ services: - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh # Local modules - - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules/material:/data/vendor/simplesamlphp/simplesamlphp/modules/material ports: - "8081:80" environment: @@ -278,7 +308,12 @@ services: - ./development/sp2-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php # Local modules - - ./modules:/data/vendor/simplesamlphp/simplesamlphp/modules + - ./modules/mfa:/data/vendor/simplesamlphp/simplesamlphp/modules/mfa + - ./modules/expirychecker:/data/vendor/simplesamlphp/simplesamlphp/modules/expirychecker + - ./modules/profilereview:/data/vendor/simplesamlphp/simplesamlphp/modules/profilereview + - ./modules/silauth:/data/vendor/simplesamlphp/simplesamlphp/modules/silauth + - ./modules/sildisco:/data/vendor/simplesamlphp/simplesamlphp/modules/sildisco + - ./modules/material:/data/vendor/simplesamlphp/simplesamlphp/modules/material ports: - "8082:80" environment: From 870cd5848d9b763d6ddf7b1e1fb20024687f9d67 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 24 May 2024 12:48:37 +0800 Subject: [PATCH 74/92] remove the "touch" command that creates default-enable all modules are now enabled by the config file because the "enable" and "default-enable" feature is going away in SimplSAMLphp 2.0 --- development/hub/run-debug.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/development/hub/run-debug.sh b/development/hub/run-debug.sh index bcb746ea..6be1b1e3 100755 --- a/development/hub/run-debug.sh +++ b/development/hub/run-debug.sh @@ -11,7 +11,6 @@ echo "xdebug.remote_enable=1" >> $INI_FILE echo "xdebug.remote_host=$XDEBUG_REMOTE_HOST" >> $INI_FILE mkdir -p /data/vendor/simplesamlphp/simplesamlphp/modules/sildisco -touch /data/vendor/simplesamlphp/simplesamlphp/modules/sildisco/default-enable # now the builtin run script can be started /data/run.sh From a488cbb44afdd9ae2796d3e3f68024abea092c86 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 24 May 2024 12:50:52 +0800 Subject: [PATCH 75/92] minor edit in README to include the word "export" on COMPOSER_CACHE_DIR --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f87a0dd4..922fd98d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ must be installed. [Make](https://www.gnu.org/software/make) is optional but simplifies the build process. -[PHP](https://www.php.net) and [Composer](https://getcomposer.org) are optional, but at a minimum you need COMPOSER_CACHE_DIR set to a local directory for storing the PHP dependency cache. This must be set in your local development environment, not in the Docker container environment. For example, in your `~/.bashrc`, include `export COMPOSER_CACHE_DIR="$HOME/.composer"` and create an empty directory at `~/.composer`. +[PHP](https://www.php.net) and [Composer](https://getcomposer.org) are optional, but at a minimum you need COMPOSER_CACHE_DIR set to a local directory for storing the PHP dependency cache. This must be exported in your local development environment, not in the Docker container environment. For example, in your `~/.bashrc`, include `export COMPOSER_CACHE_DIR="$HOME/.composer"` and create an empty directory at `~/.composer`. ## Configuration By default, configuration is read from environment variables. These are documented From 74a99b3fa71664915322fb196c545bb8c87f2617 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 24 May 2024 13:45:11 +0800 Subject: [PATCH 76/92] only run the metadata tests from run-metadata-tests.sh --- dockerbuild/run-metadata-tests.sh | 2 +- dockerbuild/run-tests.sh | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dockerbuild/run-metadata-tests.sh b/dockerbuild/run-metadata-tests.sh index b5873cb8..e8bb0678 100755 --- a/dockerbuild/run-metadata-tests.sh +++ b/dockerbuild/run-metadata-tests.sh @@ -6,4 +6,4 @@ set -x cd /data export COMPOSER_ALLOW_SUPERUSER=1; composer install -./vendor/bin/phpunit -v tests/ +./vendor/bin/phpunit -v tests/MetadataTest.php diff --git a/dockerbuild/run-tests.sh b/dockerbuild/run-tests.sh index 69884f28..e17edd55 100755 --- a/dockerbuild/run-tests.sh +++ b/dockerbuild/run-tests.sh @@ -4,4 +4,8 @@ set -e set -x /data/run-metadata-tests.sh + +./vendor/bin/phpunit -v tests/AnnouncementTest.php +./vendor/bin/phpunit -v tests/IdpDiscoTest.php + /data/run-integration-tests.sh From 9cc2f0cc76ea6a15426f9fe9d7eb58d0f028616b Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 24 May 2024 17:48:43 +0800 Subject: [PATCH 77/92] remove hub4tests remnant --- development/idp3-local/metadata/saml20-sp-remote.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/development/idp3-local/metadata/saml20-sp-remote.php b/development/idp3-local/metadata/saml20-sp-remote.php index f8c12aae..ad92fd8b 100644 --- a/development/idp3-local/metadata/saml20-sp-remote.php +++ b/development/idp3-local/metadata/saml20-sp-remote.php @@ -14,13 +14,3 @@ 'SingleLogoutService' => 'http://ssp-hub.local/module.php/sildisco/sp/saml2-logout.php/hub-discovery', 'certData' => 'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', ]; - -/* - * IdP Hub for automated tests - */ -$metadata['hub4tests'] = array( - 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', - 'AssertionConsumerService' => 'http://hub4tests/module.php/sildisco/sp/saml2-acs.php/hub-discovery', - 'SingleLogoutService' => 'http://hub4tests/module.php/sildisco/sp/saml2-logout.php/hub-discovery', - 'certData' => 'MIIDzzCCAregAwIBAgIJANuvVcQPANecMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGV2F4aGF3MQwwCgYDVQQKDANTSUwxDTALBgNVBAsMBEdUSVMxDjAMBgNVBAMMBVN0ZXZlMSQwIgYJKoZIhvcNAQkBFhVzdGV2ZV9iYWd3ZWxsQHNpbC5vcmcwHhcNMTYxMDE3MTIzMTEyWhcNMjYxMDE3MTIzMTEyWjB+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBldheGhhdzEMMAoGA1UECgwDU0lMMQ0wCwYDVQQLDARHVElTMQ4wDAYDVQQDDAVTdGV2ZTEkMCIGCSqGSIb3DQEJARYVc3RldmVfYmFnd2VsbEBzaWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAimEkw4Teyf/gZelL7OuQYg/JbDIKHPXJhLPBm/HK6pM5ZZKydVXTdMgMqkl4xK+xZ2CnkozsUiMLhAuWBsX9Dcz1M4SkPRwk4puFhXzsp7fKIVP43zUhF7p2TmbernrrIQHjg6PuegKmCGyiKUpukcYvf2RXNwHwJx+Uq0zLP4PgBSrQ2t1eKZ1jQ+noBb1NqOuy969WRYmN4EmjXDuJB9d+b3GwtbZToWgiFxFjd/NN9BFJXZEaLzRj5LAq5bu2vPPDZDarHFMRUzVJ91eafoaz6zpR1iUGj9zR+y2sUPxD/fJMZ+4AHWA2LOrTBBIuuWbp96yvcJ4WjmlfhcFQIDAQABo1AwTjAdBgNVHQ4EFgQUkJFAMJdr2lXsuezS6pDXHnmJspMwHwYDVR0jBBgwFoAUkJFAMJdr2lXsuezS6pDXHnmJspMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOEPbchaUr45L5i+ueookevsABYnltwJZ4rYJbF9VURPcEhB6JxTMZqb4s113ftHvVYfoAfLYZ9swETaHL+esx41yAebf0kWpQ3f63S5F2FcrTj+HP0XsvW/EDrvaTKM9jnKPNmbXrpq06eaUZfkVL0TAUsxYTKkttTSTiESEzp5wzYyhp7l3kpHhEvGOlh5suYjnZ2HN0uxscCR6PS47H6TMMEZuG032DWDC016/JniWvERtpf4Yw26V+I9xevp2E2MPcZne31Pe3sCh4Wpe4cV/SCFqZHlpnH96ncz4F+KvmmhbEx5VPhQSJNFIWEvI86k+lTNQOqj6YVvGvq95LQ==', -); \ No newline at end of file From 396639bfa3076f71257840f5c14cc151864130a8 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Fri, 24 May 2024 18:16:58 +0800 Subject: [PATCH 78/92] remove extraneous development files --- development/ssp/config/authsources.php | 13 ------------- development/ssp/run-debug.sh | 14 -------------- 2 files changed, 27 deletions(-) delete mode 100644 development/ssp/config/authsources.php delete mode 100755 development/ssp/run-debug.sh diff --git a/development/ssp/config/authsources.php b/development/ssp/config/authsources.php deleted file mode 100644 index 0f55fe7b..00000000 --- a/development/ssp/config/authsources.php +++ /dev/null @@ -1,13 +0,0 @@ - array( - // The default is to use core:AdminPassword, but it can be replaced with - // any authentication source. - - 'core:AdminPassword', - ), - -); diff --git a/development/ssp/run-debug.sh b/development/ssp/run-debug.sh deleted file mode 100755 index 2cc4ea19..00000000 --- a/development/ssp/run-debug.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -echo "Installing php-xdebug" -apt-get update -y -apt-get install -y php-xdebug -phpenmod xdebug - -INI_FILE="/etc/php/7.0/apache2/php.ini" -echo "Configuring debugger in $INI_FILE" -echo "xdebug.remote_enable=1" >> $INI_FILE -echo "xdebug.remote_host=$XDEBUG_REMOTE_HOST" >> $INI_FILE - -# now the builtin run script can be started -/data/run.sh From b4180172a55e40ee50536e9de4e7e0ec95471dc6 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 27 May 2024 17:03:43 +0800 Subject: [PATCH 79/92] minor tweaks to script files --- dockerbuild/run-idp.sh | 19 +++++++++---------- dockerbuild/run-integration-tests.sh | 5 ++++- dockerbuild/run-metadata-tests.sh | 5 ++++- dockerbuild/run-tests.sh | 5 ++++- dockerbuild/run.sh | 6 ++++++ 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/dockerbuild/run-idp.sh b/dockerbuild/run-idp.sh index f2faa512..d08922d0 100755 --- a/dockerbuild/run-idp.sh +++ b/dockerbuild/run-idp.sh @@ -1,17 +1,16 @@ #!/usr/bin/env bash -# Try to run database migrations -cd /data/vendor/simplesamlphp/simplesamlphp/modules/silauth -chmod a+x ./lib/Auth/Source/yii +# echo script commands to stdout +set -x + +# exit if any command fails +set -e -output=$(./lib/Auth/Source/yii migrate --interactive=0 2>&1) +# Try to run database migrations +cd /data/vendor/simplesamlphp/simplesamlphp/modules/silauth/lib/Auth/Source +chmod a+x ./yii -# If they failed, exit. -rc=$?; -if [[ $rc != 0 ]]; then - logger --priority user.err --stderr "Migrations failed with status ${rc} and output: ${output}" - exit $rc; -fi +./yii migrate --interactive=0 cd /data ./run.sh diff --git a/dockerbuild/run-integration-tests.sh b/dockerbuild/run-integration-tests.sh index f4ca4aa3..b3d92545 100755 --- a/dockerbuild/run-integration-tests.sh +++ b/dockerbuild/run-integration-tests.sh @@ -1,8 +1,11 @@ #!/usr/bin/env bash -set -e +# echo script commands to stdout set -x +# exit if any command fails +set -e + cd /data export COMPOSER_ALLOW_SUPERUSER=1; composer install diff --git a/dockerbuild/run-metadata-tests.sh b/dockerbuild/run-metadata-tests.sh index e8bb0678..f052741c 100755 --- a/dockerbuild/run-metadata-tests.sh +++ b/dockerbuild/run-metadata-tests.sh @@ -1,8 +1,11 @@ #!/usr/bin/env bash -set -e +# echo script commands to stdout set -x +# exit if any command fails +set -e + cd /data export COMPOSER_ALLOW_SUPERUSER=1; composer install diff --git a/dockerbuild/run-tests.sh b/dockerbuild/run-tests.sh index e17edd55..37c21ec7 100755 --- a/dockerbuild/run-tests.sh +++ b/dockerbuild/run-tests.sh @@ -1,8 +1,11 @@ #!/usr/bin/env bash -set -e +# echo script commands to stdout set -x +# exit if any command fails +set -e + /data/run-metadata-tests.sh ./vendor/bin/phpunit -v tests/AnnouncementTest.php diff --git a/dockerbuild/run.sh b/dockerbuild/run.sh index 748fc0b6..4d7ff730 100755 --- a/dockerbuild/run.sh +++ b/dockerbuild/run.sh @@ -1,5 +1,11 @@ #!/usr/bin/env bash +# echo script commands to stdout +set -x + +# exit if any command fails +set -e + # This is a temporary fix (bug workaround) until ssp 2.0 is in use sed -i 's_\(\\SimpleSAML\\Error\\Assertion::installHandler()\)_// \1 _' /data/vendor/simplesamlphp/simplesamlphp/www/_include.php From a07e5a8bb8d4b73678d42a5bbdeca704fcb747b5 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 27 May 2024 17:38:54 +0800 Subject: [PATCH 80/92] consistent use of maps for docker-compose environment --- actions-services.yml | 64 ++++++++++++++++++++++---------------------- docker-compose.yml | 62 +++++++++++++++++++++--------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/actions-services.yml b/actions-services.yml index e3b6b5df..523e0ded 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -23,15 +23,15 @@ services: - pwmanager.local - test-browser environment: - - MYSQL_HOST=db - - MYSQL_DATABASE=silauth - - MYSQL_USER=silauth - - MYSQL_PASSWORD=silauth - - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - - ADMIN_EMAIL=john_doe@there.com - - ADMIN_PASS=b - - SECRET_SALT=abc123 - - IDP_NAME=x + MYSQL_HOST: db + MYSQL_DATABASE: silauth + MYSQL_USER: silauth + MYSQL_PASSWORD: silauth + PROFILE_URL_FOR_TESTS: http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub + ADMIN_EMAIL: john_doe@there.com + ADMIN_PASS: b + SECRET_SALT: abc123 + IDP_NAME: x volumes: - ./dockerbuild/run-integration-tests.sh:/data/run-integration-tests.sh - ./dockerbuild/run-metadata-tests.sh:/data/run-metadata-tests.sh @@ -210,13 +210,13 @@ services: - ./development/sp2-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php environment: - - ADMIN_EMAIL=john_doe@there.com - - ADMIN_PASS=sp2 - - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz2 - - SECURE_COOKIE=false - - SHOW_SAML_ERRORS=true - - SAML20_IDP_ENABLE=false - - ADMIN_PROTECT_INDEX_PAGE=false + ADMIN_EMAIL: john_doe@there.com + ADMIN_PASS: sp2 + SECRET_SALT: h57fjemb&dn^nsJFGNjweJz2 + SECURE_COOKIE: false + SHOW_SAML_ERRORS: true + SAML20_IDP_ENABLE: false + ADMIN_PROTECT_INDEX_PAGE: false ssp-sp3.local: build: . @@ -232,13 +232,13 @@ services: - ./development/sp3-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php environment: - - ADMIN_EMAIL=john_doe@there.com - - ADMIN_PASS=sp3 - - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz3 - - SECURE_COOKIE=false - - SHOW_SAML_ERRORS=true - - SAML20_IDP_ENABLE=false - - ADMIN_PROTECT_INDEX_PAGE=false + ADMIN_EMAIL: john_doe@there.com + ADMIN_PASS: sp3 + SECRET_SALT: h57fjemb&dn^nsJFGNjweJz3 + SECURE_COOKIE: false + SHOW_SAML_ERRORS: true + SAML20_IDP_ENABLE: false + ADMIN_PROTECT_INDEX_PAGE: false pwmanager.local: @@ -253,15 +253,15 @@ services: # Utilize custom metadata - ./development/sp-local/metadata/saml20-idp-remote.php:/data/vendor/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php environment: - - ADMIN_EMAIL=john_doe@there.com - - ADMIN_PASS=sp1 - - IDP_NAME=THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED - - SECRET_SALT=NOT-a-secret-k49fjfkw73hjf9t87wjiw - - SECURE_COOKIE=false - - SHOW_SAML_ERRORS=true - - SAML20_IDP_ENABLE=false - - ADMIN_PROTECT_INDEX_PAGE=false - - THEME_USE=default + ADMIN_EMAIL: john_doe@there.com + ADMIN_PASS: sp1 + IDP_NAME: THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED + SECRET_SALT: NOT-a-secret-k49fjfkw73hjf9t87wjiw + SECURE_COOKIE: false + SHOW_SAML_ERRORS: true + SAML20_IDP_ENABLE: false + ADMIN_PROTECT_INDEX_PAGE: false + THEME_USE: default # the broker and brokerDb containers are used by the silauth module broker: diff --git a/docker-compose.yml b/docker-compose.yml index eb4939c3..c524326e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,16 +31,16 @@ services: - pwmanager.local - test-browser environment: - - MYSQL_HOST=db - - MYSQL_DATABASE=silauth - - MYSQL_USER=silauth - - MYSQL_PASSWORD=silauth - - COMPOSER_CACHE_DIR=/composer - - PROFILE_URL_FOR_TESTS=http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub - - ADMIN_EMAIL=john_doe@there.com - - ADMIN_PASS=b - - SECRET_SALT=abc123 - - IDP_NAME=x + MYSQL_HOST: db + MYSQL_DATABASE: silauth + MYSQL_USER: silauth + MYSQL_PASSWORD: silauth + COMPOSER_CACHE_DIR: /composer + PROFILE_URL_FOR_TESTS: http://pwmanager.local/module.php/core/authenticate.php?as=ssp-hub + ADMIN_EMAIL: john_doe@there.com + ADMIN_PASS: b + SECRET_SALT: abc123 + IDP_NAME: x volumes: - ./composer.json:/data/composer.json - ./composer.lock:/data/composer.lock @@ -79,7 +79,7 @@ services: env_file: - ./local.env environment: - - COMPOSER_CACHE_DIR=/composer + COMPOSER_CACHE_DIR: /composer ssp-hub.local: build: . @@ -342,13 +342,13 @@ services: env_file: - local.env environment: - - ADMIN_EMAIL=john_doe@there.com - - ADMIN_PASS=sp3 - - SECRET_SALT=h57fjemb&dn^nsJFGNjweJz3 - - SECURE_COOKIE=false - - SHOW_SAML_ERRORS=true - - SAML20_IDP_ENABLE=false - - ADMIN_PROTECT_INDEX_PAGE=false + ADMIN_EMAIL: john_doe@there.com + ADMIN_PASS: sp3 + SECRET_SALT: h57fjemb&dn^nsJFGNjweJz3 + SECURE_COOKIE: false + SHOW_SAML_ERRORS: true + SAML20_IDP_ENABLE: false + ADMIN_PROTECT_INDEX_PAGE: false pwmanager.local: image: silintl/ssp-base:develop @@ -364,15 +364,15 @@ services: ports: - "8084:80" environment: - - ADMIN_EMAIL=john_doe@there.com - - ADMIN_PASS=sp1 - - IDP_NAME=THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED - - SECRET_SALT=NOT-a-secret-k49fjfkw73hjf9t87wjiw - - SECURE_COOKIE=false - - SHOW_SAML_ERRORS=true - - SAML20_IDP_ENABLE=false - - ADMIN_PROTECT_INDEX_PAGE=false - - THEME_USE=default + ADMIN_EMAIL: john_doe@there.com + ADMIN_PASS: sp1 + IDP_NAME: THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED + SECRET_SALT: NOT-a-secret-k49fjfkw73hjf9t87wjiw + SECURE_COOKIE: false + SHOW_SAML_ERRORS: true + SAML20_IDP_ENABLE: false + ADMIN_PROTECT_INDEX_PAGE: false + THEME_USE: material:material # the broker and brokerDb containers are used by the silauth module broker: @@ -429,10 +429,10 @@ services: depends_on: - dynamo environment: - - AWS_ACCESS_KEY_ID=0 - - AWS_SECRET_ACCESS_KEY=0 - - AWS_DEFAULT_REGION=us-east-1 - - AWS_DYNAMODB_ENDPOINT=http://dynamo:8000 + AWS_ACCESS_KEY_ID: 0 + AWS_SECRET_ACCESS_KEY: 0 + AWS_DEFAULT_REGION: us-east-1 + AWS_DYNAMODB_ENDPOINT: http://dynamo:8000 node: image: node:lts-alpine From b17b32969987c885123e65d67af5274cd93b83dd Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Mon, 27 May 2024 17:46:58 +0800 Subject: [PATCH 81/92] true and false aren't valid in docker compose yml --- actions-services.yml | 24 ++++++++++++------------ docker-compose.yml | 16 ++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/actions-services.yml b/actions-services.yml index 523e0ded..abcd117f 100644 --- a/actions-services.yml +++ b/actions-services.yml @@ -213,10 +213,10 @@ services: ADMIN_EMAIL: john_doe@there.com ADMIN_PASS: sp2 SECRET_SALT: h57fjemb&dn^nsJFGNjweJz2 - SECURE_COOKIE: false - SHOW_SAML_ERRORS: true - SAML20_IDP_ENABLE: false - ADMIN_PROTECT_INDEX_PAGE: false + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + SAML20_IDP_ENABLE: "false" + ADMIN_PROTECT_INDEX_PAGE: "false" ssp-sp3.local: build: . @@ -235,10 +235,10 @@ services: ADMIN_EMAIL: john_doe@there.com ADMIN_PASS: sp3 SECRET_SALT: h57fjemb&dn^nsJFGNjweJz3 - SECURE_COOKIE: false - SHOW_SAML_ERRORS: true - SAML20_IDP_ENABLE: false - ADMIN_PROTECT_INDEX_PAGE: false + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + SAML20_IDP_ENABLE: "false" + ADMIN_PROTECT_INDEX_PAGE: "false" pwmanager.local: @@ -257,10 +257,10 @@ services: ADMIN_PASS: sp1 IDP_NAME: THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED SECRET_SALT: NOT-a-secret-k49fjfkw73hjf9t87wjiw - SECURE_COOKIE: false - SHOW_SAML_ERRORS: true - SAML20_IDP_ENABLE: false - ADMIN_PROTECT_INDEX_PAGE: false + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + SAML20_IDP_ENABLE: "false" + ADMIN_PROTECT_INDEX_PAGE: "false" THEME_USE: default # the broker and brokerDb containers are used by the silauth module diff --git a/docker-compose.yml b/docker-compose.yml index c524326e..267dbfa8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -345,10 +345,10 @@ services: ADMIN_EMAIL: john_doe@there.com ADMIN_PASS: sp3 SECRET_SALT: h57fjemb&dn^nsJFGNjweJz3 - SECURE_COOKIE: false - SHOW_SAML_ERRORS: true - SAML20_IDP_ENABLE: false - ADMIN_PROTECT_INDEX_PAGE: false + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + SAML20_IDP_ENABLE: "false" + ADMIN_PROTECT_INDEX_PAGE: "false" pwmanager.local: image: silintl/ssp-base:develop @@ -368,10 +368,10 @@ services: ADMIN_PASS: sp1 IDP_NAME: THIS VARIABLE IS REQUIRED BUT PROBABLY NOT USED SECRET_SALT: NOT-a-secret-k49fjfkw73hjf9t87wjiw - SECURE_COOKIE: false - SHOW_SAML_ERRORS: true - SAML20_IDP_ENABLE: false - ADMIN_PROTECT_INDEX_PAGE: false + SECURE_COOKIE: "false" + SHOW_SAML_ERRORS: "true" + SAML20_IDP_ENABLE: "false" + ADMIN_PROTECT_INDEX_PAGE: "false" THEME_USE: material:material # the broker and brokerDb containers are used by the silauth module From 44baf140d46ad7d7c65d238c7b9a5b91f35ac74e Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 16:26:44 +0800 Subject: [PATCH 82/92] Google analytics4 https://github.com/silinternational/simplesamlphp-module-material/pull/119 --- README.md | 2 +- .../themes/material/common-head-elements.php | 28 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 922fd98d..3f908f36 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ The login page looks for `/simplesamlphp/www/logo.png` which is **NOT** provided Update `/simplesamlphp/config/config.php`: ``` -'analytics.trackingId' => 'UA-some-unique-id-for-your-site' +'analytics.trackingId' => 'G-some-unique-id-for-your-site' ``` This project provides a convenience by loading this config with whatever is in the environment variable `ANALYTICS_ID`._ diff --git a/modules/material/themes/material/common-head-elements.php b/modules/material/themes/material/common-head-elements.php index 140d0899..ceb5690d 100644 --- a/modules/material/themes/material/common-head-elements.php +++ b/modules/material/themes/material/common-head-elements.php @@ -8,26 +8,20 @@ configuration->getValue('analytics.trackingId')); if (! empty($trackingId)) { -?> - - - - - + + + + - configuration->getValue('theme.color-scheme') ?: 'indigo-purple'); ?> From 9d1d0296f4043104497b1cd19a778fc55684e246 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 16:31:46 +0800 Subject: [PATCH 83/92] input color was too light https://github.com/silinternational/simplesamlphp-module-material/pull/136 --- modules/material/themes/material/core/loginuserpass.php | 4 ++-- .../themes/material/mfa/prompt-for-mfa-backupcode.php | 2 +- .../material/themes/material/mfa/prompt-for-mfa-manager.php | 2 +- modules/material/themes/material/mfa/prompt-for-mfa-totp.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/material/themes/material/core/loginuserpass.php b/modules/material/themes/material/core/loginuserpass.php index bc4a62af..afea971b 100644 --- a/modules/material/themes/material/core/loginuserpass.php +++ b/modules/material/themes/material/core/loginuserpass.php @@ -82,7 +82,7 @@ function onRecaptchaLoad() { data['username'] ?? null); ?> - id="username"/> @@ -91,7 +91,7 @@ function onRecaptchaLoad() { t('{material:login:label_password}') ?> - + id="password"/> diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php b/modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php index a621d002..7520467a 100644 --- a/modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php +++ b/modules/material/themes/material/mfa/prompt-for-mfa-backupcode.php @@ -39,7 +39,7 @@ - diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-manager.php b/modules/material/themes/material/mfa/prompt-for-mfa-manager.php index a385a082..e9899947 100644 --- a/modules/material/themes/material/mfa/prompt-for-mfa-manager.php +++ b/modules/material/themes/material/mfa/prompt-for-mfa-manager.php @@ -38,7 +38,7 @@ - + diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-totp.php b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php index 4b861524..c85c6718 100644 --- a/modules/material/themes/material/mfa/prompt-for-mfa-totp.php +++ b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php @@ -39,7 +39,7 @@ class="icon"> - From 3f9a018045dcdee81b20b27d9406dc4d710983fa Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 16:48:10 +0800 Subject: [PATCH 84/92] PHP 8 update https://github.com/silinternational/simplesamlphp-module-material/pull/108 --- docs/material_tests.md | 1 + modules/material/themes/material/mfa/low-on-backup-codes.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/material_tests.md b/docs/material_tests.md index a2cc2247..87a2fed0 100644 --- a/docs/material_tests.md +++ b/docs/material_tests.md @@ -115,6 +115,7 @@ _Note: This nag only works once since choosing later will simply set the nag da 1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp4** (third one) 1. Login as a "totp" user: `username=`**has_totp** `password=`**a** +1. You should see the form to enter a totp code. 1. Set up an app using this secret, `JVRXKYTMPBEVKXLS` 1. Enter code from app to verify 1. Click **Logout** diff --git a/modules/material/themes/material/mfa/low-on-backup-codes.php b/modules/material/themes/material/mfa/low-on-backup-codes.php index 152207dd..9f148a1c 100644 --- a/modules/material/themes/material/mfa/low-on-backup-codes.php +++ b/modules/material/themes/material/mfa/low-on-backup-codes.php @@ -29,7 +29,7 @@

- t('{material:mfa:running_out_info}', ['{numBackupCodesRemaining}' => (int)$this->data['numBackupCodesRemaining']]) ?> + t('{material:mfa:running_out_info}', ['{numBackupCodesRemaining}' => (string)(int)$this->data['numBackupCodesRemaining']]) ?>

From 5671793b693ba0307515edb823a1d1097cb9b9ff Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 16:56:15 +0800 Subject: [PATCH 85/92] fixed bug with float type sent to translate, added manual testing scen https://github.com/silinternational/simplesamlphp-module-material/pull/113 --- development/idp-local/config/authsources.php | 14 ++++++++++++++ docs/material_tests.md | 16 ++++++++++++++-- .../material/expirychecker/about2expire.php | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/development/idp-local/config/authsources.php b/development/idp-local/config/authsources.php index b03e4fcb..9971fe63 100644 --- a/development/idp-local/config/authsources.php +++ b/development/idp-local/config/authsources.php @@ -43,6 +43,20 @@ 'mfa' => [ 'prompt' => 'no', ], + 'schacExpiryDate' => [ + gmdate('YmdHis\Z', strtotime('+3 days')), // Soon but not tomorrow + ], + ], + + // expirychecker test user whose password expires in one day + 'next_day:a' => [ + 'eduPersonPrincipalName' => ['NEXT_DAY@ssp-hub-idp2.local'], + 'eduPersonTargetID' => ['22888888-2222-2222-2222-222222222222'], + 'sn' => ['Day'], + 'givenName' => ['Next'], + 'mail' => ['next_day@example.com'], + 'employeeNumber' => ['22888'], + 'cn' => ['NEXT_DAY'], 'schacExpiryDate' => [ gmdate('YmdHis\Z', strtotime('+1 day')), // Very soon ], diff --git a/docs/material_tests.md b/docs/material_tests.md index 87a2fed0..5e287bd1 100644 --- a/docs/material_tests.md +++ b/docs/material_tests.md @@ -54,9 +54,21 @@ See [Local Testing](../README.md#local-testing) for instructions to set up your ## Expiry functionality -### About to expire page +### About to expire page (expires in one day) -_Note: This nag only works once since choosing later will simply set the nag date into the future a little._ +_Note: This nag only works once since choosing later will simply set the nag date into the future a little. +If needed, use a new private/incognito browser window to retry.__ + +1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) +1. Click **idp2** (second one) +1. Login as an "about to expire" user: `username=`**next_day** `password=`**a** +1. Click **Later** +1. Click **Logout** + +### About to expire page (expires in three days) + +_Note: This nag only works once since choosing later will simply set the nag date into the future a little. +If needed, use a new private/incognito browser window to retry.__ 1. Goto [SP 1](http://ssp-sp1.local:8081/module.php/core/authenticate.php?as=ssp-hub-custom-port) 1. Click **idp2** (second one) diff --git a/modules/material/themes/material/expirychecker/about2expire.php b/modules/material/themes/material/expirychecker/about2expire.php index 59e8f0fe..6ef67783 100644 --- a/modules/material/themes/material/expirychecker/about2expire.php +++ b/modules/material/themes/material/expirychecker/about2expire.php @@ -31,7 +31,7 @@ $expiringMessage = $daysLeft < 2 ? $this->t('{material:about2expire:expiring_in_a_day}') : $this->t('{material:about2expire:expiring_soon}', - ['{daysLeft}' => $daysLeft]); + ['{daysLeft}' => (string)$daysLeft]); ?>

From 6ccbb260255d716c80ab0dac50620255d4035fcc Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 17:06:46 +0800 Subject: [PATCH 86/92] add logo caption https://github.com/silinternational/simplesamlphp-module-material/pull/115 https://github.com/silinternational/simplesamlphp-module-material/pull/121 https://github.com/silinternational/simplesamlphp-module-material/pull/123 --- .../material/default/selectidp-links.php | 62 ++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/modules/material/themes/material/default/selectidp-links.php b/modules/material/themes/material/default/selectidp-links.php index a5d6df67..d40d4f18 100644 --- a/modules/material/themes/material/default/selectidp-links.php +++ b/modules/material/themes/material/default/selectidp-links.php @@ -82,10 +82,14 @@ function clickedAnyway(idpName) { ?>
-
@@ -101,11 +105,15 @@ function clickedAnyway(idpName) { ?>
-
- +
+
+
+ +
+ ': htmlentities($idp['logoCaption']) ?> +
+ From 3ae33b2a8440afee076f61b1c1bf2c81199f3453 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 17:12:57 +0800 Subject: [PATCH 87/92] IDP-103 Have the browser require the username and password on login page https://github.com/silinternational/simplesamlphp-module-material/pull/125 --- modules/material/themes/material/core/loginuserpass.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/material/themes/material/core/loginuserpass.php b/modules/material/themes/material/core/loginuserpass.php index afea971b..dd950c80 100644 --- a/modules/material/themes/material/core/loginuserpass.php +++ b/modules/material/themes/material/core/loginuserpass.php @@ -82,7 +82,7 @@ function onRecaptchaLoad() { data['username'] ?? null); ?> - id="username"/> @@ -91,7 +91,7 @@ function onRecaptchaLoad() { t('{material:login:label_password}') ?> - + id="password"/> From cee96f3fdf0b1c80c1491754cedadac0da17bcd4 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 17:14:13 +0800 Subject: [PATCH 88/92] Add width related styling to the header in order to allow the word wrap to styling to function https://github.com/silinternational/simplesamlphp-module-material/pull/127/files --- modules/material/themes/material/default/selectidp-links.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/material/themes/material/default/selectidp-links.php b/modules/material/themes/material/default/selectidp-links.php index d40d4f18..44a3d00b 100644 --- a/modules/material/themes/material/default/selectidp-links.php +++ b/modules/material/themes/material/default/selectidp-links.php @@ -28,7 +28,7 @@ function clickedAnyway(idpName) {
- + data['spName'] ?? null; if (empty($spName)) { From e147782f6f757b047b28569d1dc0b7d3f5516a5c Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 17:15:52 +0800 Subject: [PATCH 89/92] Tell user which IdP the smartphone app is needed for https://github.com/silinternational/simplesamlphp-module-material/pull/129 --- .../material/themes/material/mfa/prompt-for-mfa-totp.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-totp.php b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php index c85c6718..303c0d55 100644 --- a/modules/material/themes/material/mfa/prompt-for-mfa-totp.php +++ b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php @@ -28,6 +28,13 @@ class="icon">
+
+ configuration->getValue('idp_display_name', $this->configuration->getValue('idp_name', '—'))); + ?> + (t('{material:mfa:account}', ['{idpName}' => $idpName]) ?>) +
+

t('{material:mfa:totp_instructions}') ?> From 31e3cfc166d77ffe3ab11f60b204735ca2313962 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 17:20:11 +0800 Subject: [PATCH 90/92] idp name reminder on TOTP form https://github.com/silinternational/simplesamlphp-module-material/pull/131/files --- modules/material/dictionaries/mfa.definition.json | 8 ++++---- .../material/themes/material/mfa/prompt-for-mfa-totp.php | 8 +------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/modules/material/dictionaries/mfa.definition.json b/modules/material/dictionaries/mfa.definition.json index 1d992b32..7d87e71c 100644 --- a/modules/material/dictionaries/mfa.definition.json +++ b/modules/material/dictionaries/mfa.definition.json @@ -37,10 +37,10 @@ "ko": "코드 입력" }, "totp_header": { - "en": "Smartphone app", - "es": "Aplicación de teléfono inteligente", - "fr": "Application pour smartphone", - "ko": "스마트폰 앱" + "en": "Get a code from your smartphone app", + "es": "Obtenga un código de la aplicación de su teléfono inteligente", + "fr": "Obtenez un code depuis l'application sur votre smartphone", + "ko": "스마트폰 앱에서 코드 받기" }, "totp_icon": { "en": "Smartphone app icon", diff --git a/modules/material/themes/material/mfa/prompt-for-mfa-totp.php b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php index 303c0d55..b0d10248 100644 --- a/modules/material/themes/material/mfa/prompt-for-mfa-totp.php +++ b/modules/material/themes/material/mfa/prompt-for-mfa-totp.php @@ -31,14 +31,8 @@ class="icon">

configuration->getValue('idp_display_name', $this->configuration->getValue('idp_name', '—'))); + echo $this->t('{material:mfa:account}', ['{idpName}' => $idpName]); ?> - (t('{material:mfa:account}', ['{idpName}' => $idpName]) ?>) -
- -
-

- t('{material:mfa:totp_instructions}') ?> -

From 92f26dda03aa7c92ea556b30e5f5025139ac240e Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 17:25:40 +0800 Subject: [PATCH 91/92] Improve English "Send code to manager" wording https://github.com/silinternational/simplesamlphp-module-material/pull/107/files --- .../material/dictionaries/mfa.definition.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/material/dictionaries/mfa.definition.json b/modules/material/dictionaries/mfa.definition.json index 7d87e71c..a212a988 100644 --- a/modules/material/dictionaries/mfa.definition.json +++ b/modules/material/dictionaries/mfa.definition.json @@ -134,15 +134,15 @@ }, "webauthn_error_abort": { "en": "It looks like you clicked cancel. Would you like us to try again?", - "es": "It looks like you clicked cancel. Would you like us to try again?", + "es": "Parece que has hecho clic en cancelar. ¿Quieres que lo intentemos de nuevo?", "fr": "Il semble que vous ayez cliqué sur annuler. Souhaitez-vous que nous essayions à nouveau ?", - "ko": "It looks like you clicked cancel. Would you like us to try again?" + "ko": "취소를 클릭하신 것 같습니다. 다시 시도해 보시겠어요?" }, "webauthn_error_not_allowed": { "en": "Something about that didn't work. Please ensure that your security key is plugged in and that you touch it within 60 seconds when it blinks.", - "es": "Something about that didn't work. Please ensure that your security key is plugged in and that you touch it within 60 seconds when it blinks.", + "es": "Algo de eso no funcionó. Por favor, asegúrese de que su clave de seguridad está conectada y de que la toca en un plazo de 60 segundos cuando parpadea.", "fr": "Quelque chose n'a pas fonctionné avec ça. Veuillez vous assurer que votre clé de sécurité est insérée et que vous la touchez dans les 60 secondes lorsqu'elle clignote.", - "ko": "Something about that didn't work. Please ensure that your security key is plugged in and that you touch it within 60 seconds when it blinks." + "ko": "문제가 해결되지 않았습니다. 보안 키가 연결되어 있고 깜박일 때 60초 이내에 터치했는지 확인하세요." }, "manager_icon": { "en": "Recovery contact icon", @@ -151,16 +151,16 @@ "ko": "복구 연락처 아이콘" }, "manager_header": { - "en": "Recovery contact help", - "es": "Ayuda de contacto de recuperación", - "fr": "Aide de contact de récupération", - "ko": "복구 연락처" + "en": "Ask Your Recovery Contact for Help", + "es": "Pida ayuda de contacto de recuperación", + "fr": "Demandez de l'aide à votre contact de récupération", + "ko": "복구 담당자에게 도움을 요청하십시오" }, "manager_info": { - "en": "We can send a code to your recovery contact which can be used as a temporary 2-Step Verification option. The email address on file (masked for privacy) is {managerEmail}.", - "es": "Podemos enviar un código a su contacto de recuperación que puede usarse como una opción de Verificación temporal de 2 pasos. La dirección de correo electrónico en el archivo (enmascarada por privacidad) es {managerEmail}.", - "fr": "Nous pouvons envoyer un code à votre contact de récupération, qui peut être utilisé comme option de vérification temporaire en deux étapes. L'adresse électronique au dossier (masquée pour la confidentialité) est {managerEmail}.", - "ko": "\n임시 2 단계 인증 옵션으로 사용할 수있는 코드를 복구 담당자에게 보낼 수 있습니다. 파일의 이메일 주소 (개인 정보 보호를 위해 마스크 됨)는 {managerEmail}입니다." + "en": "You can send a 2-step verification code to your recovery contact (usually your supervisor). The email we have for your recovery contact is:

{managerEmail}

We've hidden most of the letters for your contact's protection.", + "es": "Puede enviar un código de verificación de dos pasos a su contacto de recuperación (normalmente su supervisor). La dirección de correo electrónico que tenemos para su contacto de recuperación es:

{managerEmail}

Ocultamos la mayoría de las letras para proteger a su contacto.", + "fr": "Vous pouvez envoyer un code de vérification en deux étapes à votre contact de récupération (en général votre superviseur). L'adresse électronique que nous avons pour votre contact de récupération est:

{managerEmail}

. Nous avons caché la plupart des lettres pour la protection de votre contact.", + "ko": "2단계 인증 코드를 복구 연락처(보통 상사)에게 보낼 수 있습니다. 복구 연락처에 대한 이메일은 다음과 같습니다.

{managerEmail}

연락처 보호를 위해 대부분의 편지를 숨겼습니다." }, "manager_sent": { "en": "A temporary code was sent your recovery contact at {managerEmail}.", @@ -367,10 +367,10 @@ "ko": "사본" }, "button_send": { - "en": "Send", - "es": "Enviar", - "fr": "Envoyer", - "ko": "보내다" + "en": "Send code", + "es": "Enviar código", + "fr": "Envoyer code", + "ko": "코드 보내기" }, "button_cancel": { "en": "Cancel", From 5301b6992fdc809dea5f1d7651c83ef09ee6066b Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Wed, 29 May 2024 17:27:13 +0800 Subject: [PATCH 92/92] replace smartphone with authenticator IDP-24 https://github.com/silinternational/simplesamlphp-module-material/pull/134 --- .../material/dictionaries/mfa.definition.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/material/dictionaries/mfa.definition.json b/modules/material/dictionaries/mfa.definition.json index a212a988..287648eb 100644 --- a/modules/material/dictionaries/mfa.definition.json +++ b/modules/material/dictionaries/mfa.definition.json @@ -37,22 +37,22 @@ "ko": "코드 입력" }, "totp_header": { - "en": "Get a code from your smartphone app", - "es": "Obtenga un código de la aplicación de su teléfono inteligente", - "fr": "Obtenez un code depuis l'application sur votre smartphone", - "ko": "스마트폰 앱에서 코드 받기" + "en": "Get a code from your authenticator app", + "es": "Obtenga un código de su aplicación de autenticación", + "fr": "Obtenez un code depuis votre application d'authentification", + "ko": "인증 앱에서 코드 받기" }, "totp_icon": { - "en": "Smartphone app icon", + "en": "Authenticator app icon", "es": "Icono de aplicación de teléfono inteligente", "fr": "Icône de l'application Smartphone", - "ko": "스마트폰 응용 프로그램 아이콘" + "ko": "인증 응용 프로그램 아이콘" }, "totp_instructions": { - "en": "You will need to check your smartphone app for the current code.", - "es": "Deberá verificar la aplicación de su teléfono inteligente para ver el código actual.", - "fr": "Vous devriez vérifier l'application sur votre smartphone pour voir le code actuel.", - "ko": "스마트폰 앱에서 현재 코드를 확인해야합니다." + "en": "You will need to check your authenticator app for the current code.", + "es": "Deberá verificar la aplicación de autenticación para ver el código actual.", + "fr": "Vous devriez vérifier l'application d'authentification pour voir le code actuel.", + "ko": "인증 앱에서 현재 코드를 확인해야합니다." }, "totp_input": { "en": "Enter 6-digit code", @@ -283,10 +283,10 @@ "ko": "내 보안키 사용" }, "use_totp": { - "en": "Use my smartphone app instead", - "es": "Use la aplicación de mi teléfono inteligente en su lugar", - "fr": "Utiliser plutôt mon application smartphone", - "ko": "내 스마트폰 앱 사용" + "en": "Use my authenticator app instead", + "es": "Use la aplicación autenticación en su lugar", + "fr": "Utiliser plutôt mon application d'authentification", + "ko": "내 인증 앱 사용" }, "use_backupcode": { "en": "Use a printable code instead",