diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a5ffac4..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.ssh/* -.bash* -.profile -vendor/* -.sass-cache/* diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 61b0c9f..a305700 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -7,3 +7,6 @@ checks: filter: paths: [code/*, tests/*] + +tools: + external_code_coverage: true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0f7565b..10c7f50 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,22 +5,10 @@ sudo: false language: php php: - - 5.3 - - 5.4 - - 5.5 - 5.6 env: - - DB=MYSQL CORE_RELEASE=3.2 - -matrix: - include: - - php: 5.6 - env: DB=MYSQL CORE_RELEASE=3 - - php: 5.6 - env: DB=MYSQL CORE_RELEASE=3.1 - - php: 5.6 - env: DB=PGSQL CORE_RELEASE=3.2 + - DB=MYSQL CORE_RELEASE=3.6 before_script: - composer self-update || true @@ -30,4 +18,5 @@ before_script: - composer install script: - - vendor/bin/phpunit realme/tests/ + - vendor/bin/phpunit --coverage-clover=coverage.clover realme/tests/ + - bash <(curl -s https://codecov.io/bash) -f coverage.clover -R realme/ diff --git a/README.md b/README.md index 8ed28ed..3db20d9 100644 --- a/README.md +++ b/README.md @@ -9,56 +9,79 @@ silverstripe-realme Adds support to SilverStripe for authentication via [RealMe](https://www.realme.govt.nz/). -This module provides the foundation to support a quick integration for a SilverStripe application with RealMe as an +This module provides the foundation to support a quick integration for a SilverStripe application with RealMe as an identity provider. This module requires extensive setup prior to being utilised effectively. -If integration with RealMe is wanted, it is best to get in touch with the RealMe team as early as possible. There are a -number of documents mentioned in this documentation that can only be found by accessing the RealMe Shared Workspace. +If integration with RealMe is wanted, it is best to get in touch with the RealMe team as early as possible. There are a +number of documents mentioned in this documentation that can only be found by accessing the RealMe Shared Workspace. This can be accomplished by [getting in touch with the RealMe team](https://www.realme.govt.nz/realme-business/). -**Note:** Currently this module does not integrate with the `Member` functionality of SilverStripe. It is initially -intended to purely provide an authentication mechanism that can be extended by customers that require it. One such -extension would be to create standard SilverStripe `Member` records linked to a unique RealMe identifier, but that's +**Note:** Currently this module does not integrate with the `Member` functionality of SilverStripe. It is initially +intended to purely provide an authentication mechanism that can be extended by customers that require it. One such +extension would be to create standard SilverStripe `Member` records linked to a unique RealMe identifier, but that's not currently built in. +## Work in progress +This module is a work in progress. It is generally considered stable, but should have a decent knowledge of RealMe or at +least standard SAML conventions in order to debug issues. Support is provided via the GitHub Issues for this module. If +you encounter any issues, please [open a new issue here](https://github.com/silverstripe/silverstripe-realme/issues). + ## Requirements -This module doesn't have any specific requirements beyond those required by [SimpleSAMLphp](https://simplesamlphp.org): -the tool used to control authentication with the RealMe systems. +This module doesn't have any specific requirements beyond those required by +[onelogin/php-saml](https://github.com/onelogin/php-saml/blob/master/composer.json), the tool used to control +authentication with the RealMe systems. -These requirements are PHP 5.3, with the following required PHP extensions enabled: date, dom, hash, libxml, openssl, -pcre, SPL, zlib, and mcrypt php53-mcrypt +These requirements are PHP 5.6, with the following required PHP extensions enabled: date, dom, hash, libxml, openssl, +pcre, SPL, zlib, and mcrypt with the PHP bindings. -This module is designed to be run on a [CWP](https://www.cwp.govt.nz/) instance, and there are two sets of installation +This module is designed to be run on a [CWP](https://www.cwp.govt.nz/) instance, and there are two sets of installation instructions - one for use on CWP, and one for generic use. ## Installation -See the [Installation section](docs/en/installation.md) for full details. +The module is best installed via Composer, by adding the below to your composer.json. For now, we need to specify a +custom version of the excellent onelogin/php-saml module to fix some XMLDSig validation errors with the RealMe XML +responses, hence the custom `repositories` section. + +``` +{ + "require": { + "silverstripe/realme": "^2.0", + "onelogin/php-saml": "dev-fixes/realme-dsig-validation as 2.11.0" + }, + + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/madmatt/php-saml.git" + } + ] +} +``` + +Once installation is completed, configuration is required before this module will work - see below. ## Configuration of RealMe for your application -RealMe provide two testing environments and a production environment for you to integrate with. Access to these -environments is strictly controlled, and you must [contact the RealMe team](https://www.realme.govt.nz/realme-business/) -to gain access to the documentation required for these environments. +RealMe provide two testing environments and a production environment for you to integrate with. Access to these +environments is strictly controlled, and more information on these can be found on the [RealMe Developers site](https://developers.realme.govt.nz/how-to-integrate/). See [configuration.md](docs/en/configuration.md) for environment and YML configuration required before the module can be -setup. - -The configuration instructions above also steps you through setting up all three environments. +used. ## Providing RealMe login buttons -By default, the module integrates with the `Authenticator` class in SilverStripe, extending the standard SilverStripe -login form. If you want to provide your own separate login form just for RealMe, then the built-in templates can help -with this. They have been designed to integrate as cleanly as possible with SilverStripe templates, but it is up to you +By default, the module integrates with the `Authenticator` class in SilverStripe, extending the standard SilverStripe +login form. If you want to provide your own separate login form just for RealMe, then the built-in templates can help +with this. They have been designed to integrate as cleanly as possible with SilverStripe templates, but it is up to you whether you use them, or roll your own. See the [templates documentation](docs/en/templates.md) for more information on using or modifying these. ## Testing for authentication -The `RealMeService` service object allows you to inject authentication where-ever it is required. For example, let's -take a simple Controller that ensures that all users have a valid RealMe 'FLT' (a unique string that identifies a RealMe +The `RealMeService` service object allows you to inject authentication where-ever it is required. For example, let's +take a simple Controller that ensures that all users have a valid RealMe 'FLT' (a unique string that identifies a RealMe account, but is not their username. ```php @@ -67,19 +90,18 @@ class RealMeTestController extends Controller { * @var RealMeService */ public $realMeService; - + private static $dependencies = array( 'realMeService' => '%$RealMeService' ); - + public function index() { - // enforceLogin will redirect the user to RealMe if they're not authenticated, or return true if they are - // authenticated with RealMe. It should only ever return 'false' if there was an initial error dealing with - // SimpleSAMLphp + // enforceLogin will redirect the user to RealMe if they're not authenticated, or return true if they are + // authenticated with RealMe. It should only ever return 'false' if there was an error initialising config if($this->service->enforceLogin()) { $userData = $this->service->getUserData(); - - printf("Congratulations, you're authenticated with a FLT of '%s'!", $userData->UserFlt); + + printf("Congratulations, you're authenticated with a unique ID of '%s'!", $userData->SPNameID); } else { echo "There was an error while attempting to authenticate you."; } @@ -87,91 +109,6 @@ class RealMeTestController extends Controller { } ``` -### MTS: [Messaging Test Environment](https://mts.realme.govt.nz/logon-mts/home) - -The first environment is MTS. This environment is setup to allow testing of your code on your development environment. -In this environment, RealMe provide all SSL certificates required to communicate. - -- Obtain access to RealMe and the Shared Workspace for MTS public/private development keys -- Fill out the "MTS checklist" available from the shared workspace and provide to the DIA RealMe team. -- Download 'Integration Bundle Login MTS' from the [RealMe Shared Workspace](https://see.govt.nz/realme/realme/Library/Forms/Library.aspx) -- Unpack the four certificates into the directory you've specified in `REALME_CERT_DIR` (ideally outside of your webroot) - - mts_mutual_ssl_idp.cer - - mts_mutual_ssl_sp.cer - - mts_mutual_ssl_sp.pem - - mts_saml_idp.cer - - mts_saml_sp.pem - -- Run the RealMe build task to populate the configuration directories, metadata files, and authsources for MTS -```sake dev/tasks/RealMeSetupTask forEnv=mts``` - -#### MTS metadata example #### - -```xml - - - - - - - - SSL certificate info - - - - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - - - - CWP Demo Organisation - CWP Demo Organisation - http://yourdomain.govt.nz/ - - - SilverStripe - Jane - Smith - - -``` - -- Save the XML output from your task to an XML file, and upload this to the [MTS metadata upload](https://mts.realme.govt.nz/logon-mts/metadataupdate). Be sure to click continue and ok after uploading. - -- include the session data realme/templates/Layout/RealMeSessionData.ss in your template, or reference session data -directly from any descendant of SiteTree $RealMeSessionData, or by using SiteConfig: SiteConfig::current_site_config()->RealMeSessionData(); -- See the templates/Includes or templates/Layout directory for more information. - -### ITE: Integration Test Environment - -- Complete an integration to MTS. -- Obtain the ITE checklist from the RealMe shared document library and complete it. -- Publish your site to your CWP UAT environment with a working configuration for MTS and ITE -- Create a support ticket with [CWP Service desk](https://www.cwp.govt.nz/service-desk/new-request/) requesting access to ITE, and referencing information about your project, domain, and the ITE checklist - -**Note** There will be charges associated with this, as operations will need generate and purchase the SSL certificates required for your domain, and provide them to DIA -To save time, both ITE and production certificates will be purchased at the same time. - -If you wish do do this process yourself please see the [ssl-certs documentation](docs/en/ssl-certs.md) - -### PROD: Production Environment - -- Complete an integration to MTS and ITE. -- Obtain the Production checklist from the RealMe shared document library and complete it. -- Publish your site to your CWP UAT environment with a working configuration for MTS and ITE and a configuration for production -- Create a support ticket with [CWP Service desk](https://www.cwp.govt.nz/service-desk/new-request/) requesting access to production, and referencing information about your project, domain, and the production checklist - -**Note** There will be charges associated with this, as operations will need generate and purchase the SSL certificates required for your domain, and provide them to DIA - -If you wish do do this process yourself please see the [ssl-certs documentation](docs/en/ssl-certs.md) - -## Known issues -The RelayState must be less than 80 bytes +## Appreciation +* Sincere thanks to Jackson (@jakxnz) for his work reviewing and updating pull requests. \ No newline at end of file diff --git a/_config.php b/_config.php index 5a129ab..a1535e1 100644 --- a/_config.php +++ b/_config.php @@ -1,5 +1,9 @@ load($cacheKey)) { + if ($cache->test($cacheKey)) { return true; } // check we have config constants present. - $configs = array('REALME_CONFIG_DIR', 'REALME_CERT_DIR', 'REALME_LOG_DIR', 'REALME_TEMP_DIR'); - foreach ($configs as $config) { - if (false === defined($config)) { - SS_Log::log( - sprintf('RealMe config not set: %s', $config), - SS_Log::ERR - ); - return false; - }; + if (!defined('REALME_CERT_DIR')) { + SS_Log::log('RealMe env config REALME_CERT_DIR not set', SS_Log::ERR); + return false; + }; - $path = rtrim(constant($config), '/'); - if (false === file_exists($path) || false === is_readable($path)) { - SS_Log::log( - sprintf('RealMe config dir missing or not readable: %s', $config), - SS_Log::ERR - ); - return false; - } + $path = rtrim(constant('REALME_CERT_DIR'), '/'); + if (!file_exists($path) || !is_readable($path)) { + SS_Log::log('RealMe certificate dir (REALME_CERT_DIR) missing or not readable', SS_Log::ERR); + return false; } // Check certificates (cert dir must exist at this point). - $certificates = array('REALME_SIGNING_CERT_FILENAME', 'REALME_MUTUAL_CERT_FILENAME'); - foreach ($certificates as $cert) { - $path = rtrim(REALME_CERT_DIR, '/') . "/" . constant($cert); - if (false === file_exists($path) || false === is_readable($path)) { - SS_Log::log( - sprintf('RealMe %s missing: %s', $cert, $path), - SS_Log::ERR - ); - return false; - } - } - - // if we haven't created this file, or it doesn't exist, then the setup task hasn't run correctly. - $authSource = REALME_CONFIG_DIR . '/authsources.php'; - if (false === file_exists($authSource)) { - SS_Log::log( - sprintf('RealMe Setup task not complete. missing auth source: %s', $authSource), - SS_Log::ERR - ); + $path = rtrim(REALME_CERT_DIR, '/') . "/" . constant('REALME_SIGNING_CERT_FILENAME'); + if (!file_exists($path) || !is_readable($path)) { + SS_Log::log(sprintf('RealMe %s missing: %s', constant('REALME_SIGNING_CERT_FILENAME'), $path), SS_Log::ERR); return false; } - $cache->save('true', $cacheKey); + $cache->save('1', $cacheKey); return true; } diff --git a/code/RealMeLoginForm.php b/code/RealMeLoginForm.php index 4f10872..d66e78c 100644 --- a/code/RealMeLoginForm.php +++ b/code/RealMeLoginForm.php @@ -26,6 +26,24 @@ class RealMeLoginForm extends LoginForm */ private static $widget_theme; + /** + * @config + * @var string The service name to display in the login box ("To access the [online service], you need a RealMe login.") + */ + private static $service_name_1 = null; + + /** + * @config + * @var string The service name to display in the What's RealMe popup header ("To log in to [this service] you need a RealMe login.") + */ + private static $service_name_2 = null; + + /** + * @config + * @var string The service name to display in the What's RealMe popup text ("[This service] uses RealMe login.") + */ + private static $service_name_3 = null; + /** * @var array */ @@ -33,6 +51,8 @@ class RealMeLoginForm extends LoginForm 'redirectToRealMe' ); + protected static $action_button_name = 'redirectToRealMe'; + /** * @var string The authentication class tied to this login form */ @@ -46,16 +66,27 @@ class RealMeLoginForm extends LoginForm */ public function __construct($controller, $name) { + /** @var RealMeService $service */ + $service = Injector::inst()->get('RealMeService'); + $integrationType = $service->config()->integration_type; + $fields = new FieldList(array( new HiddenField('AuthenticationMethod', null, $this->authenticator_class) )); - $loginButtonContent = ArrayData::create(array( - 'Label' => _t('RealMeLoginForm.LOGINBUTTON', 'Login or register with RealMe') - ))->renderWith('RealMeLoginButton'); + if($integrationType === RealMeService::TYPE_ASSERT) { + $loginButtonContent = ArrayData::create(array( + 'Label' => _t('RealMeLoginForm.ASSERTLOGINBUTTON', 'Share your details with {orgname}', '', ['orgname' => $service->config()->metadata_organisation_display_name]) + ))->renderWith('RealMeLoginButton'); + } else { + // Login button + $loginButtonContent = ArrayData::create(array( + 'Label' => _t('RealMeLoginForm.LOGINBUTTON', 'Login') + ))->renderWith('RealMeLoginButton'); + } $actions = new FieldList(array( - FormAction::create('redirectToRealMe', _t('RealMeLoginForm.LOGINBUTTON', 'LoginAction')) + FormAction::create(self::$action_button_name, _t('RealMeLoginForm.LOGINBUTTON', 'LoginAction')) ->setUseButtonTag(true) ->setButtonContent($loginButtonContent) ->setAttribute('class', 'realme_button') @@ -64,8 +95,8 @@ public function __construct($controller, $name) // Taken from MemberLoginForm if (isset($_REQUEST['BackURL'])) { $backURL = $_REQUEST['BackURL']; - } elseif (Session::get('BackURL')) { - $backURL = Session::get('BackURL'); + } elseif (Session::get('RealMeBackURL')) { + $backURL = Session::get('RealMeBackURL'); } if (isset($backURL)) { @@ -96,7 +127,7 @@ public function __construct($controller, $name) * * @param array $data * @param Form $form - * @return SS_HTTPResponse|void If successfully processed, returns void (SimpleSAMLphp redirects to RealMe) + * @return SS_HTTPResponse If successfully processed, returns void (SimpleSAMLphp redirects to RealMe) * @throws SS_HTTPResponse_Exception */ public function redirectToRealMe($data, Form $form) @@ -143,4 +174,70 @@ public function getRealMeWidgetTheme() return 'default'; } + + /** + * Gets the service name based on either a config value, or falling back to the $Title specified in SiteConfig + * @param string $name The service name to get from config + * @return string + */ + private function getServiceName($name = 'service_name_1') + { + if ($this->config()->$name) { + return $this->config()->$name; + } else { + return SiteConfig::current_site_config()->Title; + } + } + + public function getServiceName1() + { + return $this->getServiceName('service_name_1'); + } + + public function getServiceName2() + { + return $this->getServiceName('service_name_2'); + } + + public function getServiceName3() + { + return $this->getServiceName('service_name_3'); + } + + public function forTemplate() + { + /** @var RealMeService $service */ + $service = Injector::inst()->get('RealMeService'); + $integrationType = $service->config()->integration_type; + + if($integrationType === RealMeService::TYPE_ASSERT) { + $html = $this->renderWith([ + 'RealMeAssertForm' + ]); + + // Now that we've rendered, clear message + $this->clearMessage(); + + return $html; + } else { + return parent::forTemplate(); + } + } + + /** + * Returns the last error message that the RealMe service provided, if any + * @return string|null + */ + public function RealMeLastError() + { + $message = Session::get('RealMe.LastErrorMessage'); + Session::clear('RealMe.LastErrorMessage'); + + return $message; + } + + public function HasRealMeLastError() + { + return Session::get('RealMe.LastErrorMessage') !== null; + } } diff --git a/code/RealMeMiniLoginForm.php b/code/RealMeMiniLoginForm.php new file mode 100644 index 0000000..40aed74 --- /dev/null +++ b/code/RealMeMiniLoginForm.php @@ -0,0 +1,53 @@ +setFormMethod('GET', true); + + $buttonName = sprintf('action_%s', self::$action_button_name); + $this->Actions()->fieldByName($buttonName)->addExtraClass('mini'); + } + + public function getRealMeMiniLoginLink() + { + $fields = $this->Fields(); + $buttonName = sprintf('action_%s', self::$action_button_name); + $action = $this->Actions()->fieldByName($buttonName); + + $authMethod = $fields->dataFieldByName('AuthenticationMethod')->Value(); + $token = $fields->dataFieldByName('SecurityID')->Value(); + $actionName = $action->getName(); + $actionValue = _t('RealMeLoginForm.LOGINBUTTON', 'LoginAction'); + + $queryString = sprintf('?AuthenticationMethod=%s&SecurityID=%s&%s=%s', $authMethod, $token, $actionName, $actionValue); + return Controller::join_links($this->FormAction(), $queryString); + } + + public function getMiniLoginFormPopupPosition() + { + return sprintf('realme_arrow_top_%s', $this->popupPosition); + } + + /** + * The mini login form can either have the popup appear below and to the left or right. When creating the form, call + * $form->setMiniLoginFormPopupPosition(), with the first arg being either 'left' or 'right'. This is actually the + * 'arrow' position, so it's the opposite of what you expect (in other words, if you set it to 'left', the box will + * extend out to the right under the mini login form. + */ + public function setMiniLoginFormPopupPosition($dir) + { + if (!in_array($dir, [ 'left', 'right' ])) { + $dir = 'left'; + } + + $this->popupPosition = $dir; + } +} \ No newline at end of file diff --git a/code/RealMeService.php b/code/RealMeService.php index ce0fc6f..0e15be0 100644 --- a/code/RealMeService.php +++ b/code/RealMeService.php @@ -1,5 +1,5 @@ null, self::ENV_ITE => null, self::ENV_PROD => null ); + /** + * @config + * @var array Stores the default identity provider (IdP) entity IDs. These can be customised if you're using an + * intermediary IdP instead of connecting to RealMe directly. + */ + private static $idp_entity_ids = array( + self::ENV_MTS => array( + self::TYPE_LOGIN => 'https://mts.realme.govt.nz/saml2', + self::TYPE_ASSERT => 'https://mts.realme.govt.nz/realmemts/realmeidp', + ), + self::ENV_ITE => array( + self::TYPE_LOGIN => 'https://www.ite.logon.realme.govt.nz/saml2', + self::TYPE_ASSERT => 'https://www.ite.account.realme.govt.nz/saml2/assertion', + ), + self::ENV_PROD => array( + self::TYPE_LOGIN => 'https://www.logon.realme.govt.nz/saml2', + self::TYPE_ASSERT => 'https://www.account.realme.govt.nz/saml2/assertion', + ) + ); + + private static $idp_sso_service_urls = array( + self::ENV_MTS => array( + self::TYPE_LOGIN => 'https://mts.realme.govt.nz/logon-mts/mtsEntryPoint', + self::TYPE_ASSERT => 'https://mts.realme.govt.nz/realme-mts/validate/realme-mts-idp.xhtml' + ), + self::ENV_ITE => array( + self::TYPE_LOGIN => 'https://www.ite.logon.realme.govt.nz/sso/logon/metaAlias/logon/logonidp', + self::TYPE_ASSERT => 'https://www.ite.assert.realme.govt.nz/sso/SSORedirect/metaAlias/assertion/realmeidp' + ), + self::ENV_PROD => array( + self::TYPE_LOGIN => 'https://www.logon.realme.govt.nz/sso/logon/metaAlias/logon/logonidp', + self::TYPE_ASSERT => 'https://www.assert.realme.govt.nz/sso/SSORedirect/metaAlias/assertion/realmeidp' + ) + ); + + /** + * @var array A list of certificate filenames for different RealMe environments and integration types. These files + * must be located in the directory specified by the REALME_CERT_DIR environment variable. These filenames are the + * same as the files that can be found in the RealMe Shared Workspace, within the 'Integration Bundle' ZIP files for + * the different environments (MTS, ITE and Production), so you just need to extract the specific certificate file + * that you need and make sure it's in place on the server in the REALME_CERT_DIR. + */ + private static $idp_x509_cert_filenames = array( + self::ENV_MTS => array( + self::TYPE_LOGIN => 'mts_login_saml_idp.cer', + self::TYPE_ASSERT => 'mts_assert_saml_idp.cer' + ), + self::ENV_ITE => array( + self::TYPE_LOGIN => 'ite.signing.logon.realme.govt.nz.cer', + self::TYPE_ASSERT => 'ite.signing.account.realme.govt.nz.cer' + ), + self::ENV_PROD => array( + self::TYPE_LOGIN => 'signing.logon.realme.govt.nz.cer', + self::TYPE_ASSERT => 'signing.account.realme.govt.nz.cer' + ) + ); + /** * @config * @var array Stores the AuthN context values for each supported RealMe environment. This needs to be setup prior to @@ -125,34 +194,11 @@ class RealMeService extends Object self::AUTHN_MOD_TOKEN_SID ); - - /** - * @config - * @var array Stores the proxy_host values used when creating the back-channel SoapClient connection to the RealMe - * artifact resolution service. This can either be: - * - null (indicating no proxy is required), - * - a plain string (e.g. gateway.your-network.govt.nz), - * - the name of an environment variable that can be called (via getenv()) to retrieve the proxy URL from - * (e.g. env:http_proxy). In this case, it is assumed that a full URL would exist in this environment variable - * (e.g. tcp://gateway.your-network.govt.nz:8080) as it is intended to be used to mimic how curl handles HTTP - * proxy (if you specify the http_proxy env-var, curl will automatically parse it as a full URL and use that - * for resolving all requests by default. - */ - private static $backchannel_proxy_hosts = array( - self::ENV_MTS => null, - self::ENV_ITE => null, - self::ENV_PROD => null - ); - /** * @config - * @var array Stores the proxy_port values used when creating the back-channel SoapClient connection to the RealMe - * artifact resolution service. - * - * See the definition for self::$backchannel_proxy_hosts for more information on the - * valid values. + * @var array Domain names for metadata files. Used in @link RealMeSetupTask when outputting metadata XML */ - private static $backchannel_proxy_ports = array( + private static $metadata_assertion_service_domains = array( self::ENV_MTS => null, self::ENV_ITE => null, self::ENV_PROD => null @@ -160,12 +206,20 @@ class RealMeService extends Object /** * @config - * @var array Domain names for metadata files. Used in @link RealMeSetupTask when outputting metadata XML + * @var array A list of error messages to display if RealMe returns error statuses, instead of the default + * translations (found in realme/lang/en.yml for example). */ - private static $metadata_assertion_service_domains = array( - self::ENV_MTS => null, - self::ENV_ITE => null, - self::ENV_PROD => null + private static $realme_error_message_overrides = array( + self::ERR_AUTHN_FAILED => null, + self::ERR_TIMEOUT => null, + self::ERR_INTERNAL_ERROR => null, + self::ERR_NO_AVAILABLE_IDP => null, + self::ERR_REQUEST_UNSUPPORTED => null, + self::ERR_NO_PASSIVE => null, + self::ERR_REQUEST_DENIED => null, + self::ERR_UNSUPPORTED_BINDING => null, + self::ERR_UNKNOWN_PRINCIPAL => null, + self::ERR_NO_AUTHN_CONTEXT => null ); /** @@ -205,380 +259,387 @@ class RealMeService extends Object private static $metadata_contact_support_surname = null; /** - * @return bool true if the user is correctly authenticated, false if there was an error with login - * NB: If the user is not authenticated, they will be redirected to RealMe to login, so a boolean false return here - * indicates that there was a failure during the authentication process (perhaps a communication issue) + * @var OneLogin_Saml2_Auth|null Set by {@link getAuth()}, which creates an instance of OneLogin_Saml2_Auth to check + * authentication against */ - public function enforceLogin() - { - $auth = new SimpleSAML_Auth_Simple($this->config()->auth_source_name); - - $auth->requireAuth(array( - 'ReturnTo' => '/Security/realme/acs', - 'ErrorURL' => '/Security/realme/error' - )); + private $auth = null; - $loggedIn = false; - $authData = $this->getAuthData($auth); + /** + * @var string|null The last error message during login enforcement + */ + private $lastError = null; - if (is_null($authData)) { - // no-op, $loggedIn stays false and no data is written - } else { - $this->config()->user_data = $authData; - Session::set('RealMeSessionDataSerialized', serialize($authData)); - $loggedIn = true; - } - return $loggedIn; + /** + * @return array + */ + public static function get_template_global_variables() + { + return array( + 'RealMeUser' => array( + 'method' => 'current_realme_user' + ) + ); } /** - * Clear the RealMe credentials from Session, and also remove SimpleSAMLphp session information. - * @return void + * Return the user data which was saved to session from the first RealMe + * auth. + * Note: Does not check authenticity or expiry of this data + * + * @return RealMeUser */ - public function clearLogin() + public static function user_data() { - Session::clear('RealMeSessionDataSerialized'); - $this->config()->__set('user_data', null); + if(!is_null(static::$user_data)){ + return static::$user_data; + } - $session = SimpleSAML_Session::getSessionFromRequest(); + $sessionData = Session::get('RealMe.SessionData'); + + // Exit point + if(is_null($sessionData)) { + return null; + } - if ($session instanceof SimpleSAML_Session) { - $session->doLogout($this->config()->auth_source_name); + // Unserialise stored data + $user = unserialize($sessionData); + if($user == false || !$user instanceof RealMeUser) { + return null; } + + static::$user_data = $user; + return static::$user_data; } /** - * Return the user data which was saved to session from the first RealMe auth. - * Note: Does not check authenticity or expiry of this data + * Calls available user data and checks for validity * - * @return ArrayData + * @return RealMeUser */ - public function getUserData() + public static function current_realme_user() { - if (is_null($this->config()->user_data)) { - $sessionData = Session::get('RealMeSessionDataSerialized'); - - if (!is_null($sessionData) && unserialize($sessionData) !== false) { - $this->config()->user_data = unserialize($sessionData); - } + $user = self::user_data(); + if($user && !$user->isValid()) { + return null; } - return $this->config()->user_data; + return $user; } /** - * @param SimpleSAML_Auth_Simple $auth The authentication context as returned from RealMe - * @return ArrayData + * A helpful static method that follows silverstripe naming for + * Member::currentUser(); + * + * @return RealMeUser */ - private function getAuthData(SimpleSAML_Auth_Simple $auth) + public static function currentRealMeUser() { - // returns null if the current auth is invalid or timed out. - $data = $auth->getAuthDataArray(); - $returnedData = null; - - if ( - is_array($data) && - isset($data['saml:sp:IdP']) && - isset($data['saml:sp:NameID']) && - is_array($data['saml:sp:NameID']) && - isset($data['saml:sp:NameID']['Value']) && - isset($data['Expire']) && - isset($data['Attributes']) && - isset($data['saml:sp:SessionIndex']) - ) { - $returnedData = new ArrayData(array( - 'NameID' => new ArrayData($data['saml:sp:NameID']), - 'UserFlt' => $data['saml:sp:NameID']['Value'], - 'Attributes' => new ArrayData($data['Attributes']), - 'Expire' => $data['Expire'], - 'SessionIndex' => $data['saml:sp:SessionIndex'] - )); - } - return $returnedData; + return self::current_realme_user(); } /** - * @return string A BackURL as specified originally when accessing /Security/login, for use after authentication + * Enforce login via RealMe. This can be used in controllers to force users to be authenticated via RealMe (not + * necessarily logged in as a {@link Member}), in the form of: + * + * Session::set('RealMeBackURL', '/path/to/the/controller/method'); + * if($service->enforceLogin()) { + * // User has a valid RealMe account, $service->getAuthData() will return you their details + * } else { + * // Something went wrong processing their details, show an error + * } + * + * + * In cases where people are *not* authenticated with RealMe, this method will redirect them directly to RealMe. + * + * However, generally you want this to be an explicit process, so you should look at instead using the standard + * {@link RealMeAuthenticator}. + * + * A return value of bool false indicates that there was a failure during the authentication process (perhaps a + * communication issue, or a failure to decode the response correctly. You should handle this like you would any + * other unexpected authentication error. You can use {@link getLastError()} to see if a human-readable error + * message exists for display to the user. + * + * @return bool|null true if the user is correctly authenticated, false if there was an error with login */ - public function getBackURL() + public function enforceLogin() { - if (!empty($_REQUEST['BackURL'])) { - $url = $_REQUEST['BackURL']; - } elseif (Session::get('BackURL')) { - $url = Session::get('BackURL'); - Session::clear('BackURL'); // Ensure we don't redirect back to the same error twice + // First, check to see if we have an existing authenticated session + if($this->isAuthenticated()) { + return true; } - if (isset($url) && Director::is_site_url($url)) { - $url = Director::absoluteURL($url); - } else { - // Spoofing attack or no back URL set, redirect to homepage instead of spoofing url - $url = Director::absoluteBaseURL(); + // If not, attempt to retrieve authentication data from OneLogin (in case this is called during SAML assertion) + try { + if(!Session::get("RealMeErrorBackURL")){ + Session::set("RealMeErrorBackURL", Controller::curr()->Link("Login")); + } + + $this->getAuth()->processResponse(); + + // if there were any errors from the SAML request, process and translate them. + $errors = $this->getAuth()->getErrors(); + if(is_array($errors) && !empty($errors)) { + $this->processSamlErrors($errors); + return false; + } + + $authData = $this->getAuthData(); + + // If no data is found, then force login + if(is_null($authData)) { + throw new RealMeException('No SAML data, enforcing login', RealMeException::NOT_AUTHENTICATED); + } + + // call a success method as we've successfully logged in (if it exists) + Member::singleton()->extend('onRealMeLoginSuccess', $authData); + + // Sync with local member + $this->syncWithLocalMemberDatabase(); + + } catch(Exception $e) { + Member::singleton()->extend("onRealMeLoginFailure", $e); + + // No auth data or failed to decrypt, enforce login again + $this->getAuth()->login(Director::absoluteBaseURL()); + die; } - return $url; + return $this->getAuth()->isAuthenticated(); } /** - * @return string|null Either the directory where SimpleSAMLphp configuration is stored, or null if undefined + * If there was an error returned from the saml response, process the errors + * + * @param $errors */ - public function getSimpleSamlConfigDir() - { - return (defined('REALME_CONFIG_DIR') ? rtrim(REALME_CONFIG_DIR, '/') : null); - } + private function processSamlErrors(array $errors){ + $translatedMessage = null; - /** - * @return string The path to SimpleSAMLphp's metadata. This will either be defined in config, or just '/metadata' - */ - public function getSimpleSamlMetadataDir() - { - return sprintf('%s/metadata', $this->getSimpleSamlConfigDir()); - } + // The error message returned by onelogin/php-saml is the top-level error, but we want the actual error + $request = Controller::curr()->getRequest(); + if($request->isPOST() && $request->postVar("SAMLResponse")) { - /** - * @return string Either the value for baseurlpath in SimpleSAML's config, or a default value if it's been unset - */ - public function getSimpleSamlBaseUrlPath() - { - if (strlen($this->config()->simplesaml_base_url_path) > 0) { - return $this->config()->simplesaml_base_url_path; - } else { - return 'simplesaml/'; + $response = new OneLogin_Saml2_Response($this->getAuth()->getSettings(), $request->postVar("SAMLResponse")); + $internalError = OneLogin_Saml2_Utils::query($response->document, "/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode/@Value"); + + if($internalError instanceof DOMNodeList && $internalError->length > 0) { + $internalErrorCode = $internalError->item(0)->textContent; + $translatedMessage = $this->findErrorMessageForCode($internalErrorCode); + } } - } - /** - * @return string|null Either the directory where certificates are stored, or null if undefined - */ - public function getCertDir() - { - return (defined('REALME_CERT_DIR') ? REALME_CERT_DIR : null); - } + // If we found a message to display, then let's redirect to the form and display it + if($translatedMessage) { + $this->lastError = $translatedMessage; + } - /** - * @return string|null Either the directory where logging information is kept by SimpleSAMLphp, or null if undefined - */ - public function getLoggingDir() - { - return (defined('REALME_LOG_DIR') ? REALME_LOG_DIR : null); + SS_Log::log( + sprintf( + 'onelogin/php-saml error messages: %s (%s)', + join(', ', $errors), + $this->getAuth()->getLastErrorReason() + ), + SS_Log::ERR + ); } /** - * @return string|null Either the directory where temp files can be written by SimpleSAMLphp, or null if undefined + * Checks data stored in Session to see if the user is authenticated. + * @return bool true if the user is authenticated via RealMe and we can trust ->getUserData() */ - public function getTempDir() + public function isAuthenticated() { - return (defined('REALME_TEMP_DIR') ? REALME_TEMP_DIR : null); + $user = $this->getUserData(); + return $user instanceof RealMeUser && $user->isAuthenticated(); } /** - * This looks first to a Config variable that can be set in YML configuration, and falls back to generating a - * salted SHA256-hashed password. To generate a password in this format, see the bin/pwgen.php file in the - * SimpleSAMLphp vendor directory (normally vendor/madmatt/simplesamlphp/bin/pwgen.php). If setting a password - * via Config, ensure it contains {SSHA256} at the start of the line. + * Returns a {@link RealMeUser} object if one can be built from the RealMe session data. * - * @return string|null The administrator password set for SimpleSAMLphp. If null, it means a strong hash couldn't be - * created due to the code being deployed on an older machine, and a generated password will need to be set. + * @throws OneLogin_Saml2_Error Passes on the SAML error if it's not indicating a lack of SAML response data + * @throws RealMeException If identity information exists but couldn't be decoded, or doesn't exist + * @return RealMeUser|null */ - public function findOrMakeSimpleSAMLPassword() + public function getAuthData() { - if (strlen($this->config()->simplesaml_hashed_admin_password) > 0) { - $password = $this->config()->simplesaml_hashed_admin_password; + // returns null if the current auth is invalid or timed out. + try { + // Process response and capture details + $auth = $this->getAuth(); + + if(!$auth->isAuthenticated()) { + throw new RealMeException( + 'OneLogin SAML library did not successfully authenticate, but did not return a specific error', + RealMeException::NOT_AUTHENTICATED + ); + } - if (strpos($password, '{SSHA256}') !== 0) { - $password = null; // Ensure password is salted SHA256 + $spNameId = $auth->getNameId(); + if(!is_string($spNameId)) { + throw new RealMeException('Invalid/Missing NameID in SAML response', RealMeException::MISSING_NAMEID); + } + + $sessionIndex = $auth->getSessionIndex(); + if(!is_string($sessionIndex)) { + throw new RealMeException( + 'Invalid/Missing SessionIndex value in SAML response', + RealMeException::MISSING_SESSION_INDEX + ); + } + + $attributes = $auth->getAttributes(); + if(!is_array($attributes)) { + throw new RealMeException( + 'Invalid/Missing attributes array in SAML response', + RealMeException::MISSING_ATTRIBUTES + ); } - } else { - $salt = openssl_random_pseudo_bytes(8, $strongSalt); // SHA256 needs 8 bytes - $password = openssl_random_pseudo_bytes(32, $strongPassword); // Make a random 32-byte password - if (!$strongSalt || !$strongPassword || !$salt || !$password) { - $password = null; // Ensure the password is strong, return null if we can't guarantee a strong one + $federatedIdentity = $this->retrieveFederatedIdentity($auth); + + // We will have either a FLT or FIT, depending on integration type + if($this->config()->integration_type == self::TYPE_ASSERT) { + $userTag = $this->retrieveFederatedIdentityTag($auth); } else { - $hash = hash('sha256', $password.$salt, true); - $password = sprintf('{SSHA256}%s', base64_encode($hash.$salt)); + $userTag = $this->retrieveFederatedLogonTag($auth); } - } - return $password; + return new RealMeUser([ + 'SPNameID' => $spNameId, + 'UserFederatedTag' => $userTag, + 'SessionIndex' => $sessionIndex, + 'Attributes' => $attributes, + 'FederatedIdentity' => $federatedIdentity + ]); + } catch(OneLogin_Saml2_Error $e) { + // If the Exception code indicates there wasn't a response, we ignore it as it simply means the visitor + // isn't authenticated yet. Otherwise, we re-throw the Exception + if($e->getCode() === OneLogin_Saml2_Error::SAML_RESPONSE_NOT_FOUND) { + return null; + } else { + throw $e; + } + } } /** - * @return string A 32-byte salt string for SimpleSAML to use when signing content + * Clear the RealMe credentials from Session, called during Security->logout() overrides + * @return void */ - public function generateSimpleSAMLSalt() + public function clearLogin() { - if (strlen($this->config()->simplesaml_secret_salt) > 0) { - $salt = $this->config()->simplesaml_secret_salt; - } else { - $salt = base64_encode(openssl_random_pseudo_bytes(32, $strongSalt)); - - if (!$salt || !$strongSalt) { - $salt = null; // Ensure salt is strong, return null if we can't generate a strong one - } - } + $this->config()->__set('user_data', null); - return $salt; + Session::set("RealMeBackURL", null); + Session::set("RealMeErrorBackURL", null); + Session::set("RealMe.SessionData", null); + Session::set("RealMe.OriginalResponse", null); + Session::set("RealMe.LastErrorMessage", null); } - /** - * Returns the appropriate entity ID for RealMe, given the environment passed in. The entity ID may be different per - * environment, and should be a full URL, including privacy realm and application name. For example, this may be: - * https://www.agency.govt.nz/privacy-realm-name/application-name - * - * @param string $env The environment to return the entity ID for. Must be one of the RealMe environment names - * @return string|null Returns the entity ID for the given $env, or null if no entity ID exists - */ - public function getEntityIDForEnvironment($env) + public function getLastError() { - return $this->getConfigurationVarByEnv('entity_ids', $env); + return $this->lastError; } /** - * Returns the appropriate AuthN Context, given the environment passed in. The AuthNContext may be different per - * environment, and should be one of the strings as defined in the static {@link self::$authn_contexts} at the top - * of this class. + * Helper method, alias for RealMeService::user_data() * - * @param string $env The environment to return the AuthNContext for. Must be one of the RealMe environment names - * @return string|null Returns the AuthNContext for the given $env, or null if no context exists + * @return RealMeUser */ - public function getAuthnContextForEnvironment($env) + public function getUserData() { - return $this->getConfigurationVarByEnv('authn_contexts', $env); + return self::user_data(); } /** - * Gets the proxy host (if required) for back-channel SOAP requests. The proxy host can begin with the string 'env:' - * in which case the script will call getenv() on the returned value and attempt to parse it as a full URL. This is - * designed primarily to be compatible with the 'http_proxy' that curl uses by default. In other words, passing in - * `env:http_proxy` is the equivalent of saying 'use the same HTTP proxy that curl will use in this environment'. - * - * @param string $env The environment to return the proxy_host for. Must be one of the RealMe environment names - * @return string|null Returns the SOAPClient `proxy_host` param, or null if there isn't one + * @return string A BackURL as specified originally when accessing /Security/login, for use after authentication */ - public function getProxyHostForEnvironment($env) + public function getBackURL() { - $host = $this->getConfigurationVarByEnv('backchannel_proxy_hosts', $env); - - // Allow usage of an environment variable to define this - if (substr($host, 0, 4) === 'env:') { - $host = getenv(substr($host, 4)); - - if ($host === false) { - // getenv() didn't return a valid environment var, it's either mis-spelled or doesn't exist - $host = null; - } else { - $host = parse_url($host, PHP_URL_HOST); + $url = null; - // This may happen on seriously malformed URLs, in which case we should return null - if ($host === false) { - $host = null; - } - } + if(Session::get('RealMeBackURL')) { + $url = Session::get('RealMeBackURL'); + Session::clear('RealMeBackURL'); // Ensure we don't redirect back to the same error twice } - return $host; + return $this->validSiteURL($url); } - /** - * Gets the proxy port (if required) for back-channel SOAP requests. The proxy port can begin with the string 'env:' - * in which case the script will call getenv() on the returned value and attempt to parse it as a full URL. This is - * designed primarily to be compatible with the 'http_proxy' that curl uses by default. In other words, passing in - * `env:http_proxy` is the equivalent of saying 'use the same HTTP proxy that curl will use in this environment'. - * - * @param string $env The environment to return the proxy_port for. Must be one of the RealMe environment names - * @return string|null Returns the SOAPClient `proxy_port` param, or null if there isn't one - */ - public function getProxyPortForEnvironment($env) + public function getErrorBackURL() { - $port = $this->getConfigurationVarByEnv('backchannel_proxy_ports', $env); + $url = null; - // Allow usage of an environment variable to define this - if (substr($port, 0, 4) === 'env:') { - $port = getenv(substr($port, 4)); + if(Session::get('RealMeErrorBackURL')) { + $url = Session::get('RealMeErrorBackURL'); + Session::clear('RealMeErrorBackURL'); // Ensure we don't redirect back to the same error twice + } - if($port === false) { - // getenv() didn't return a valid environment var, it's either mis-spelled or doesn't exist - $port = null; - } else { - $port = parse_url($port, PHP_URL_PORT); + return $this->validSiteURL($url); + } - // This may happen on seriously malformed URLs, in which case we should return null - if ($port === false) { - $port = null; - } - } + private function validSiteURL($url = null) + { + if(isset($url) && Director::is_site_url($url)) { + $url = Director::absoluteURL($url); + } else { + // Spoofing attack or no back URL set, redirect to homepage instead of spoofing url + $url = Director::absoluteBaseURL(); } - return $port; + return $url; } /** - * @param string $cfgName The static configuration value to get. This should be an array - * @param string $env The environment to return the value for. Must be one of the RealMe environment names - * @return string|null Returns the value as defined in $cfgName for the given environment, or null if none exist + * @param String $subdir A sub-directory where certificates may be stored for + * a specific case + * @return string|null Either the directory where certificates are stored, + * or null if undefined */ - private function getConfigurationVarByEnv($cfgName, $env) + public function getCertDir($subdir = null) { - $value = null; - if (in_array($env, $this->getAllowedRealMeEnvironments())) { - $values = $this->config()->$cfgName; + // Trim prepended seprator to avoid absolute path + $path = ltrim(ltrim($subdir, '/'), '\\'); - if (is_array($values) && isset($values[$env])) { - $value = $values[$env]; - } + if(defined('REALME_CERT_DIR')) { + $path = REALME_CERT_DIR . '/' . $path; // Duplicate slashes will be handled by realpath() } - return $value; + return realpath($path); } /** - * Returns the full path to the SAML signing certificate file, used by SimpleSAMLphp to sign all messages sent to - * RealMe. + * Returns the appropriate AuthN Context, given the environment passed in. The AuthNContext may be different per + * environment, and should be one of the strings as defined in the static {@link self::$authn_contexts} at the top + * of this class. * - * @return string|null Either the full path to the SAML signing certificate file, or null if it doesn't exist + * @param string $env The environment to return the AuthNContext for. Must be one of the RealMe environment names + * @return string|null Returns the AuthNContext for the given $env, or null if no context exists */ - public function getSigningCertPath() + public function getAuthnContextForEnvironment($env) { - return $this->getCertPath('SIGNING'); + return $this->getConfigurationVarByEnv('authn_contexts', $env); } /** - * Returns the full path to the mutual back-channel certificate file, used by SimpleSAMLphp to communicate securely - * with RealMe when connecting to the RealMe Assertion Resolution Service (Artifact Resolver). + * Returns the full path to the SAML signing certificate file, used by SimpleSAMLphp to sign all messages sent to + * RealMe. * - * @return string|null Either the full path to the SAML mutual certificate file, or null if it doesn't exist + * @return string|null Either the full path to the SAML signing certificate file, or null if it doesn't exist */ - public function getMutualCertPath() + public function getSigningCertPath() { - return $this->getCertPath('MUTUAL'); + return $this->getCertPath('SIGNING'); } - /** - * @param string $certName The certificate name, either 'SIGNING' or 'MUTUAL' - * @return string|null Either the full path to the certificate file, or null if it doesn't exist - * @see self::getSigningCertPathForEnvironment(), self::getMutualCertPathForEnvironment() - */ - private function getCertPath($certName) + public function getIdPCertPath() { - $certPath = null; - $certDir = $this->getCertDir(); + $cfg = $this->config(); + $name = $this->getConfigurationVarByEnv('idp_x509_cert_filenames', $cfg->realme_env, $cfg->integration_type); - if (in_array($certName, array('SIGNING', 'MUTUAL'))) { - $constName = sprintf('REALME_%s_CERT_FILENAME', strtoupper($certName)); - if (defined($constName)) { - $filename = constant($constName); - $certPath = Controller::join_links($certDir, $filename); - } - } - - // Ensure the file exists, if it doesn't then set it to null - if (!is_null($certPath) && !file_exists($certPath)) { - $certPath = null; - } - - return $certPath; + return $this->getCertDir($name); } /** @@ -587,51 +648,67 @@ private function getCertPath($certName) * certificates used for ITE and production don't need one. * * @return string|null Either the password, or null if there is no password. + * @deprecated 3.0 */ public function getSigningCertPassword() { return (defined('REALME_SIGNING_CERT_PASSWORD') ? REALME_SIGNING_CERT_PASSWORD : null); } - /** - * Returns the password (if any) necessary to decrypt the mutual back-channel cert specified by - * self::getSigningCertPath(). If no password is set, then this method returns null. MTS certificates require a - * password, however generally the certificates used for ITE and production don't need one. - * - * @return string|null Either the password, or null if there is no password. - */ - public function getMutualCertPassword() + public function getSPCertContent($contentType = 'certificate') + { + return $this->getCertificateContents($this->getSigningCertPath(), $contentType); + } + + public function getIdPCertContent() { - return (defined('REALME_MUTUAL_CERT_PASSWORD') ? REALME_MUTUAL_CERT_PASSWORD : null); + return $this->getCertificateContents($this->getIdPCertPath(), 'certificate'); } /** - * Returns the content of the SAML signing certificate. This is used by @link RealMeSetupTask to output metadata. - * The metadata file requires just the certificate to be included, without the BEGIN/END CERTIFICATE lines + * Returns the content of the SAML signing certificate. This is used by getAuth() and by RealMeSetupTask to produce + * metadata XML files. + * + * @param string $certPath The filesystem path to where the certificate is stored on the filesystem + * @param string $contentType Either 'certificate' or 'key', depending on which part of the file to return * @return string|null The content of the signing certificate */ - public function getSigningCertContent() + public function getCertificateContents($certPath, $contentType = 'certificate') { - $certPath = $this->getSigningCertPath(); - $certificate = null; + $text = null; if (!is_null($certPath)) { $certificateContents = file_get_contents($certPath); - // This is a PEM key, and we need to extract just the certificate, stripping out the private key etc. - // So we search for everything between '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----' - preg_match( - '/-----BEGIN CERTIFICATE-----\n([^-]*)\n-----END CERTIFICATE-----/', - $certificateContents, - $matches - ); + // If the file does not contain any header information and the content type is certificate, just return it + if($contentType == 'certificate' && !preg_match('/-----BEGIN/', $certificateContents)) { + $text = $certificateContents; + } else { + // Otherwise, inspect the file and match based on the full contents + if($contentType == 'certificate') { + $pattern = '/-----BEGIN CERTIFICATE-----[\r\n]*([^-]*)[\r\n]*-----END CERTIFICATE-----/'; + } elseif($contentType == 'key') { + $pattern = '/-----BEGIN [A-Z ]*PRIVATE KEY-----\n([^-]*)\n-----END [A-Z ]*PRIVATE KEY-----/'; + } else { + throw new InvalidArgumentException('Argument contentType must be either "certificate" or "key"'); + } + + // This is a PEM key, and we need to extract just the certificate, stripping out the private key etc. + // So we search for everything between '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----' + preg_match( + $pattern, + $certificateContents, + $matches + ); - if (isset($matches) && is_array($matches) && isset($matches[1])) { - $certificate = $matches[1]; + if (isset($matches) && is_array($matches) && isset($matches[1])) { + $text = trim($matches[1]); + } } + } - return $certificate; + return $text; } /** @@ -640,29 +717,17 @@ public function getSigningCertContent() */ public function getAssertionConsumerServiceUrlForEnvironment($env) { - if (false === in_array($env, $this->getAllowedRealMeEnvironments())) { + if (in_array($env, $this->getAllowedRealMeEnvironments()) === false) { return null; } - // Returns http://domain.govt.nz/vendor/madmatt/simplesamlphp/www/module.php/saml/sp/saml2-acs.php/realme-mts $domain = $this->getMetadataAssertionServiceDomainForEnvironment($env); - if (false === filter_var($domain, FILTER_VALIDATE_URL)) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { return null; } - $basePath = $this->getSimpleSamlBaseUrlPath(); - $modulePath = 'module.php/saml/sp/saml2-acs.php/'; - $authSource = sprintf('realme-%s', $env); - return Controller::join_links($domain, $basePath, $modulePath, $authSource); - } - - /** - * @param string $env The environment to return the domain name for. Must be one of the RealMe environment names - * @return string|null Either the FQDN (e.g. https://www.realme-demo.govt.nz/) or null if none is specified - */ - private function getMetadataAssertionServiceDomainForEnvironment($env) - { - return $this->getConfigurationVarByEnv('metadata_assertion_service_domains', $env); + // Returns https://domain.govt.nz/Security/realme/acs + return Controller::join_links($domain, 'Security/realme/acs'); } /** @@ -693,7 +758,7 @@ public function getMetadataOrganisationUrl() } /** - * @return array The support contact details to be used in metadata XML output, with null values if they don't exist + * @return string[] The support contact details to be used in metadata XML output, with null values if they don't exist */ public function getMetadataContactSupport() { @@ -725,4 +790,346 @@ public function getAllowedAuthNContextList() { return $this->config()->allowed_authn_context_list; } + + /** + * Returns the appropriate entity ID for RealMe, given the environment passed in. The entity ID may be different per + * environment, and should be a full URL, including privacy realm and application name. For example, this may be: + * https://www.agency.govt.nz/privacy-realm-name/application-name + * + * @return string|null Returns the entity ID for the current environment, or null if no entity ID exists + */ + public function getSPEntityID() + { + return $this->getConfigurationVarByEnv('sp_entity_ids', $this->config()->realme_env); + } + + private function getIdPEntityID() + { + $cfg = $this->config(); + return $this->getConfigurationVarByEnv('idp_entity_ids', $cfg->realme_env, $cfg->integration_type); + } + + private function getSingleSignOnServiceURL() + { + $cfg = $this->config(); + return $this->getConfigurationVarByEnv('idp_sso_service_urls', $cfg->realme_env, $cfg->integration_type); + } + + private function getRequestedAuthnContext() + { + return $this->getConfigurationVarByEnv('authn_contexts', $this->config()->realme_env); + } + + /** + * Returns the internal {@link OneLogin_Saml2_Auth} object against which visitors are authenticated. + * + * @return OneLogin_Saml2_Auth + */ + public function getAuth() + { + if(isset($this->auth)) return $this->auth; + + // If we're behind a trusted proxy, force onelogin to use the HTTP_X_FORWARDED_FOR headers to determine + // protocol, host and port + if(TRUSTED_PROXY) { + OneLogin_Saml2_Utils::setProxyVars(true); + } + + $settings = [ + 'strict' => true, + 'debug' => false, + + // Service Provider (this installation) configuration + 'sp' => [ + 'entityId' => $this->getSPEntityID(), + 'x509cert' => $this->getSPCertContent('certificate'), + 'privateKey' => $this->getSPCertContent('key'), + + // According to RealMe messaging spec, must always be transient for assert; is irrelevant for login + 'NameIDFormat' => $this->getNameIdFormat(), + + 'assertionConsumerService' => [ + 'url' => $this->getAssertionConsumerServiceUrlForEnvironment($this->config()->realme_env), + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' // Always POST, not artifact binding + ] + ], + + // RealMe Identity Provider configuration + 'idp' => [ + 'entityId' => $this->getIdPEntityID(), + 'x509cert' => $this->getIdPCertContent(), + + 'singleSignOnService' => [ + 'url' => $this->getSingleSignOnServiceURL(), + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + ] + ], + + 'security' => [ + 'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'authnRequestsSigned' => true, + 'wantAssertionsEncrypted' => true, + 'wantAssertionsSigned' => true, + + 'requestedAuthnContext' => [ + $this->getRequestedAuthnContext() + ] + ] + ]; + + $this->auth = new OneLogin_Saml2_Auth($settings); + return $this->auth; + } + + /** + * @return string the required NameIDFormat to be included in metadata XML, based on the requested integration type + */ + public function getNameIdFormat() + { + switch ($this->config()->integration_type) { + case self::TYPE_ASSERT: + return 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'; + break; + + case self::TYPE_LOGIN: + default: + return 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'; + break; + } + } + + /** + * @param string $cfgName The static configuration value to get. This should be an array + * @param string $env The environment to return the value for. Must be one of the RealMe environment names + * @param string $integrationType The integration type (login or assert), if necessary, to determine return var + * @throws InvalidArgumentException If the cfgVar doesn't exist, or is malformed + * @return string|null Returns the value as defined in $cfgName for the given environment, or null if none exist + */ + private function getConfigurationVarByEnv($cfgName, $env, $integrationType = null) + { + $value = null; + + if (in_array($env, $this->getAllowedRealMeEnvironments())) { + $values = $this->config()->$cfgName; + + if (is_array($values) && isset($values[$env])) { + $value = $values[$env]; + } + } + + // If $integrationType is specified, then $value should be an array, with the array key being the integration + // type and array value being the returned variable + if(!is_null($integrationType) && is_array($value) && isset($value[$integrationType])) { + $value = $value[$integrationType]; + } elseif(!is_null($integrationType)) { + // Otherwise, we are expecting an integration type, but the value is not specified that way, error out + throw new InvalidArgumentException( + sprintf( + 'Config value %s[%s][%s] not well formed (cfg var not an array)', + $cfgName, + $env, + $integrationType + ) + ); + } + + if(is_null($value)) { + throw new InvalidArgumentException(sprintf('Config value %s[%s] not set', $cfgName, $env)); + } + + return $value; + } + + /** + * @param string $certName The certificate name, either 'SIGNING' or 'MUTUAL' + * @return string|null Either the full path to the certificate file, or null if it doesn't exist + * @see self::getSigningCertPath() + */ + private function getCertPath($certName) + { + $certPath = null; + + if (in_array($certName, array('SIGNING', 'MUTUAL'))) { + $constName = sprintf('REALME_%s_CERT_FILENAME', strtoupper($certName)); + if (defined($constName)) { + $certPath = $this->getCertDir(constant($constName)); + } + } + + // Ensure the file exists, if it doesn't then set it to null + if (!is_null($certPath) && !file_exists($certPath)) { + $certPath = null; + } + + return $certPath; + } + + /** + * @param string $env The environment to return the domain name for. Must be one of the RealMe environment names + * @return string|null Either the FQDN (e.g. https://www.realme-demo.govt.nz/) or null if none is specified + */ + private function getMetadataAssertionServiceDomainForEnvironment($env) + { + return $this->getConfigurationVarByEnv('metadata_assertion_service_domains', $env); + } + + /** + * @param OneLogin_Saml2_Auth $auth + * @return string|null null if there's no FLT, or a string if there is one + */ + private function retrieveFederatedLogonTag(OneLogin_Saml2_Auth $auth) + { + return null; // @todo + } + + /** + * @param OneLogin_Saml2_Auth $auth + * @return string|null null if there's not FIT, or a string if there is one + */ + private function retrieveFederatedIdentityTag(OneLogin_Saml2_Auth $auth) + { + $fit = null; + $attributes = $auth->getAttributes(); + + if(isset($attributes['urn:nzl:govt:ict:stds:authn:attribute:igovt:IVS:FIT'])) { + $fit = $attributes['urn:nzl:govt:ict:stds:authn:attribute:igovt:IVS:FIT'][0]; + } + + return $fit; + } + + /** + * @param OneLogin_Saml2_Auth $auth + * @return RealMeFederatedIdentity|null + * @throws RealMeException + */ + private function retrieveFederatedIdentity(OneLogin_Saml2_Auth $auth) + { + $federatedIdentity = null; + $attributes = $auth->getAttributes(); + $nameId = $auth->getNameId(); + + // If identity information exists, retrieve the FIT (Federated Identity Tag) and identity data + if(isset($attributes['urn:nzl:govt:ict:stds:authn:safeb64:attribute:igovt:IVS:Assertion:Identity'])) { + // Identity information is encoded using 'Base 64 Encoding with URL and Filename Safe Alphabet' + // For more info, review RFC3548, section 4 (https://tools.ietf.org/html/rfc3548#page-6) + // Note: This is different to PHP's standard base64_decode() function, therefore we need to swap chars + // to match PHP's expectations: + // char 62 (-) becomes + + // char 63 (_) becomes / + + $identity = $attributes['urn:nzl:govt:ict:stds:authn:safeb64:attribute:igovt:IVS:Assertion:Identity']; + + if(!is_array($identity) || !isset($identity[0])) { + throw new RealMeException( + 'Invalid identity response received from RealMe', + RealMeException::INVALID_IDENTITY_VALUE + ); + } + + // Switch from filename-safe alphabet base64 encoding to standard base64 encoding + $identity = strtr($identity[0], '-_', '+/'); + $identity = base64_decode($identity, true); + + if(is_bool($identity) && !$identity) { + // Strict base64_decode fails, either the identity didn't exist or was mangled during transmission + throw new RealMeException( + 'Failed to parse safe base64 encoded identity', + RealMeException::FAILED_PARSING_IDENTITY + ); + } + + $identityDoc = new DOMDocument(); + if($identityDoc->loadXML($identity)) { + $federatedIdentity = new RealMeFederatedIdentity($identityDoc, $nameId); + } + } + + return $federatedIdentity; + } + + /** + * Called by {@link enforceLogin()} when visitor has been authenticated. If the config value + * RealMeService.sync_with_local_member_database === true, then either create or update a local {@link Member} + * object to include details provided by RealMe. + */ + private function syncWithLocalMemberDatabase() { + if($this->config()->sync_with_local_member_database === true) { + $member = DataObject::get_one("Member", + sprintf("RealmeSPNameID = '%s'", Convert::raw2sql($this->getAuthData()->SPNameID)) + ); + + if(!$member){ + $member = Member::create(["RealmeSPNameID" => $this->getAuthData()->SPNameID]); + $member->write(); + } + + if($this->config()->login_member_after_authentication){ + $member->logIn(); + } + } + } + + /** + * Finds a human-readable error message based on the error code provided in the RealMe SAML response + * + * @return string|null The human-readable error message, or null if one can't be found + */ + private function findErrorMessageForCode($errorCode) { + $message = null; + $messageOverrides = $this->config()->realme_error_message_overrides; + + switch($errorCode) { + case self::ERR_AUTHN_FAILED: + $message = _t('RealMeService.ERROR_AUTHNFAILED'); + break; + + case self::ERR_TIMEOUT: + $message = _t('RealMeService.ERROR_TIMEOUT'); + break; + + case self::ERR_INTERNAL_ERROR: + $message = _t('RealMeService.ERROR_INTERNAL'); + break; + + case self::ERR_NO_AVAILABLE_IDP: + $message = _t('RealMeService.ERROR_NOAVAILABLEIDP'); + break; + + case self::ERR_REQUEST_UNSUPPORTED: + $message = _t('RealMeService.ERROR_REQUESTUNSUPPORTED'); + break; + + case self::ERR_NO_PASSIVE: + $message = _t('RealMeService.ERROR_NOPASSIVE'); + break; + + case self::ERR_REQUEST_DENIED: + $message = _t('RealMeService.ERROR_REQUESTDENIED'); + break; + + case self::ERR_UNSUPPORTED_BINDING: + $message = _t('RealMeService.ERROR_UNSUPPORTEDBINDING'); + break; + + case self::ERR_UNKNOWN_PRINCIPAL: + $message = _t('RealMeService.ERROR_UNKNOWNPRINCIPAL'); + break; + + case self::ERR_NO_AUTHN_CONTEXT: + $message = _t('RealMeService.ERROR_NOAUTHNCONTEXT'); + break; + + default: + $message = _t('RealMeService.ERROR_GENERAL'); + break; + } + + // Allow message overrides if they exist + if(array_key_exists($errorCode, $messageOverrides) && !is_null($messageOverrides[$errorCode])) { + $message = $messageOverrides[$errorCode]; + } + + return $message; + } } diff --git a/code/exceptions/RealMeException.php b/code/exceptions/RealMeException.php new file mode 100644 index 0000000..2f42cb7 --- /dev/null +++ b/code/exceptions/RealMeException.php @@ -0,0 +1,12 @@ + '%$RealMeService' - ); - - /** - * @var RealMeService - */ - public $service; - - /** - * - */ - public function RealMeSessionData() - { - return $this->service->getUserData(); - } -} diff --git a/code/extensions/RealMeMemberExtension.php b/code/extensions/RealMeMemberExtension.php new file mode 100644 index 0000000..abd5f5d --- /dev/null +++ b/code/extensions/RealMeMemberExtension.php @@ -0,0 +1,15 @@ + "Varchar(35)", + ); + + private static $indexes = array( + "RealmeSPNameID" => true + ); +} \ No newline at end of file diff --git a/code/extensions/RealMeSecurityExtension.php b/code/extensions/RealMeSecurityExtension.php index 5f24042..6c04a66 100644 --- a/code/extensions/RealMeSecurityExtension.php +++ b/code/extensions/RealMeSecurityExtension.php @@ -2,16 +2,6 @@ class RealMeSecurityExtension extends Extension { - /** - * Error constants used for business logic and switching error messages - */ - const AUTHN_FAILED = 'urn:oasis:names:tc:SAML:2.0:status:AuthnFailed'; - const TIMEOUT = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:status:Timeout'; - const UNKNOWN_PRINCIPAL = 'urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal'; - const INTERNAL_ERROR = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:status:InternalError'; - const NO_AVAILABLE_IDP = 'urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP'; - const GENERAL_ERROR = ''; - private static $allowed_actions = array( 'realme' ); @@ -50,11 +40,13 @@ public function beforeCallActionHandler($request, $action) */ private function realMeLogout($redirect = true) { + Session::clear('RealMe'); $this->service->clearLogin(); if ($redirect) { return $this->owner->logout($redirect); } else { + $this->owner->logout(); return $this->owner->redirectBack(); } } @@ -73,9 +65,6 @@ public function realme() case 'acs': return $this->realMeACS(); - case 'error': - return $this->realMeErrorHandler(); - case 'logout': return $this->realMeLogout(); @@ -91,97 +80,54 @@ public function realme() */ private function realMeACS() { - $loggedIn = $this->service->enforceLogin(); + try { + $authenticated = $this->service->enforceLogin(); - if (true === $loggedIn) { - return $this->owner->redirect($this->service->getBackURL()); - } + if ($authenticated === true) { + $authData = $this->service->getAuthData(); - return Security::permissionFailure( - $this->owner, - _t( - 'RealMeSecurityExtension.LOGINFAILURE', - 'Unfortunately we\'re not able to log you in through RealMe right now.' - ) - ); - } + // If more session vars are set here, they must be cleared in realmeLogout() + Session::set('RealMe.SessionData', serialize($authData)); + Session::set('RealMe.OriginalResponse', $_POST['SAMLResponse']); - /** - * Process the error/Exception returned from SimpleSaml and return an appropriate error to the user. - * - * @return SS_HTTPResponse - */ - private function realMeErrorHandler() - { - // Error handling, to prevent infinite login loops if there was an internal error with SimpleSAMLphp - if ($exceptionId = $this->owner->getRequest()->getVar('SimpleSAML_Auth_State_exceptionId')) { - if (is_string($exceptionId) && strlen($exceptionId) > 1) { - $authState = SimpleSAML_Auth_State::loadExceptionState($exceptionId); - if (true === array_key_exists('SimpleSAML_Auth_State.exceptionData', $authState) - && $authState['SimpleSAML_Auth_State.exceptionData'] instanceof sspmod_saml_Error) { - $exception = $authState['SimpleSAML_Auth_State.exceptionData']; - $message = $this->getErrorMessage($exception); - - SS_Log::log( - sprintf('Error while validating RealMe authentication details: %s', $message), - SS_Log::ERR - ); - - return Security::permissionFailure($this->owner, $message); + // If a redirect has not already been set, then redirect to the default BackURL + if (!$this->owner->redirectedTo()) { + return $this->owner->redirect($this->service->getBackURL()); + } + } else { + if (is_string($this->service->getLastError())) { + Session::set('RealMe.LastErrorMessage', $this->service->getLastError()); + + // Redirect to the 'Error Back URL' if set + $backUrl = $this->service->getErrorBackURL(); + if ($backUrl) { + return $this->owner->redirect($backUrl); + } else { + // Fallback to homepage + return $this->owner->redirect('/'); + } } + throw new RealMeException( + 'Attempted access of RealMeSecurityExtension->realMeACS() without SAML response', + RealMeException::MISSING_AUTHN_RESPONSE + ); } + } catch (Exception $e) { + $msg = sprintf( + 'Error during RealMe authentication process. Code: %d, Message: %s', + $e->getCode(), + $e->getMessage() + ); + + SS_Log::log($msg, SS_Log::ERR); } - SS_Log::log('Unknown error while attempting to parse RealMe authentication', SS_Log::ERR); - return Security::permissionFailure( $this->owner, - _t('RealMeSecurityExtension.GENERAL_ERROR', '', - array('errorMsg' => 'Unknown') + _t( + 'RealMeSecurityExtension.LOGINFAILURE', + 'Unfortunately we\'re not able to log you in through RealMe right now. Please try again shortly.' ) ); } - - /** - * Return the realme error message associated with a SimpleSAML error. - * - * @param $exception sspmod_saml_Error - * - * @return string - */ - private function getErrorMessage($exception) - { - switch ($exception->getSubStatus()) { - - // if the identity provider goes down, it usually means something like the SMS service is down. - case self::NO_AVAILABLE_IDP: - return _t('RealMeSecurityExtension.NO_AVAILABLE_IDP', '', array('errorMsg' => $exception->getMessage())); - - // Usually means your entity ID is miss-matched against this server metadata (re-upload metadata), - // but can mean first time users need to use a specific setting. - case self::UNKNOWN_PRINCIPAL: - return _t('RealMeSecurityExtension.UNKNOWN_PRINCIPAL', '', array('errorMsg' => $exception->getMessage())); - - // Something went terribly wrong at realme. - case self::INTERNAL_ERROR: - return _t('RealMeSecurityExtension.INTERNAL_ERROR', '', array('errorMsg' => $exception->getMessage())); - - // General time out - case self::TIMEOUT: - return _t('RealMeSecurityExtension.TIMEOUT', '', array('errorMsg' => $exception->getMessage())); - - // They logged out from realme. - case self::AUTHN_FAILED: - return _t('RealMeSecurityExtension.AUTHN_FAILED', '', array('errorMsg' => $exception->getMessage())); - - // Give the general error for all others: REQUEST_UNSUPPORTED,UNSUPPORTED_BINDING,,REQUEST_DENIED or unknown. - default : - return _t('RealMeSecurityExtension.GENERAL_ERROR', - "RealMe reported a serious application error with the message [{errorMsg}]. " . - "Please try again later. If the problem persists, please contact RealMe Help " . - "Desk on 0800 664 774.", - array('errorMsg' => $exception->getMessage()) - ); - } - } } diff --git a/code/model/RealMeFederatedIdentity.php b/code/model/RealMeFederatedIdentity.php new file mode 100644 index 0000000..9919d0f --- /dev/null +++ b/code/model/RealMeFederatedIdentity.php @@ -0,0 +1,209 @@ +get('RealMeService')->enforceLogin(); // Enforce login and ensure auth data exists + * $identity = Injector::inst()->get('RealMeService')->getAuthData()->getIdentity(); + * + * Notes: + * - We can't store the original DOMDocument as it's not possible to properly serialize and unserialize this such that + * it can be stored in session. Therefore, during object instantiation, we parse the XML, and store individual details + * directly against properties. + * + * - See this object's constructor for the XML / DOMDocument object expected to be passed during instantiation. + */ +class RealMeFederatedIdentity extends ViewableData +{ + + /** + * @var string The FIT (Federated Identity Tag) for this identity. This is the unique string that identifies an + * individual, and should generally be mapped one-to-one with a {@link Member} object + */ + private $nameId; + + /** + * @var string The given first name(s) of the federated identity returned by RealMe. + */ + public $FirstName; + + /** + * @var string The given middle name(s) of the federated identity returned by RealMe. + */ + public $MiddleName; + + /** + * @var string The given last name of the federated identity returned by RealMe. + */ + public $LastName; + + /** + * @var string The gender of the federated identity returned by RealMe. Will be one of 'M', 'F', possibly 'U' or 'O' + * (messaging specs are unclear). + */ + public $Gender; + + /** + * @var DOMNodeList Undocumented in RealMe messaging spec, generally describes the quality of birth info based + * presumably on the source. + */ + public $BirthInfoQuality; + + /** + * @var string The birth year of the federated identity returned by RealMe, e.g. 1993, 1954, 2015. + * Probably better to use {@link getDateOfBirth()} which will return an {@link SS_Datetime} object. + */ + public $BirthYear; + + /** + * @var string The birth month of the federated identity returned by RealMe, e.g. 05 (May). + * Probably better to use {@link getDateOfBirth()} which will return an {@link SS_Datetime} object. + */ + public $BirthMonth; + + /** + * @var string The birth day of the federated identity returned by RealMe, e.g. 05 (5th day of the month). + * Probably better to use {@link getDateOfBirth()} which will return an {@link SS_Datetime} object. + */ + public $BirthDay; + + /** + * @var string Undocumented in RealMe messaging spec, generally describes the quality of birthplace info based + * presumably on the source. + */ + public $BirthPlaceQuality; + + /** + * @var string The country of birth for the given federated identity returned by RealMe. + */ + public $BirthPlaceCountry; + + /** + * @var string The birthplace 'locality' of the federated identity returned by RealMe, e.g. 'Wellington', 'Unknown' + */ + public $BirthPlaceLocality; + + /** + * Constructor that sets the expected federated identity details based on a provided DOMDocument. The expected XML + * structure for the DOMDocument is the following: + * + * + * + * + * + * Edmund + * Percival + * Hillary + * + * + * + * + * 1919 + * 07 + * 20 + * + * + * New Zealand + * + * + * Auckland + * + * + * + * + * + * @param DOMDocument $identity + * @param string $nameId + */ + public function __construct(DOMDocument $identity, $nameId) + { + parent::__construct(); + $this->nameId = $nameId; + + $xpath = new DOMXPath($identity); + $xpath->registerNamespace('p', 'urn:oasis:names:tc:ciq:xpil:3'); + $xpath->registerNamespace('dataQuality', 'urn:oasis:names:tc:ciq:ct:3'); + $xpath->registerNamespace('n', 'urn:oasis:names:tc:ciq:xnl:3'); + $xpath->registerNamespace('xlink', 'http://www.w3.org/1999/xlink'); + $xpath->registerNamespace('addr', 'urn:oasis:names:tc:ciq:xal:3'); + + // Name elements + $this->FirstName = $this->getNodeValue($xpath, "/p:Party/p:PartyName/n:PersonName/n:NameElement[@n:ElementType='FirstName']"); + $this->MiddleName = $this->getNodeValue($xpath, "/p:Party/p:PartyName/n:PersonName/n:NameElement[@n:ElementType='MiddleName']"); + $this->LastName = $this->getNodeValue($xpath, "/p:Party/p:PartyName/n:PersonName/n:NameElement[@n:ElementType='LastName']"); + + // Gender + $this->Gender = $this->getNamedItemNodeValue($xpath, '/p:Party/p:PersonInfo[@p:Gender]', 'Gender'); + + // Birth info + $this->BirthInfoQuality = $xpath->query("/p:Party/p:BirthInfo[@dataQuality:DataQualityType]"); + + // Birth date + $this->BirthYear = $this->getNodeValue($xpath, "/p:Party/p:BirthInfo/p:BirthInfoElement[@p:Type='BirthYear']"); + $this->BirthMonth = $this->getNodeValue($xpath, "/p:Party/p:BirthInfo/p:BirthInfoElement[@p:Type='BirthMonth']"); + $this->BirthDay = $this->getNodeValue($xpath, "/p:Party/p:BirthInfo/p:BirthInfoElement[@p:Type='BirthDay']"); + + // Birth place + $this->BirthPlaceQuality = $this->getNamedItemNodeValue($xpath, '/p:Party/p:BirthInfo/p:BirthPlaceDetails[@dataQuality:DataQualityType]', 'DataQualityType'); + $this->BirthPlaceCountry = $this->getNodeValue($xpath, "/p:Party/p:BirthInfo/p:BirthPlaceDetails/addr:Country/addr:NameElement[@addr:NameType='Name']"); + $this->BirthPlaceLocality = $this->getNodeValue($xpath, "/p:Party/p:BirthInfo/p:BirthPlaceDetails/addr:Locality/addr:NameElement[@addr:NameType='Name']"); + } + + public function isValid() + { + return true; + } + + public function getDateOfBirth() + { + if($this->BirthYear && $this->BirthMonth && $this->BirthDay) { + $value = sprintf('%d-%d-%d', $this->BirthYear, $this->BirthMonth, $this->BirthDay); + return DBField::create_field('SS_DateTime', $value); + } else { + return null; + } + } + + /** + * @param DOMXPath $xpath The DOMXPath object to carry out the query on + * @param string $query The XPath query to find the relevant node + * @param string $namedAttr The named attribute to retrieve from the XPath query + * @return string|null Either the value from the named item, or null if no item exists + */ + private function getNamedItemNodeValue(DOMXPath $xpath, $query, $namedAttr) + { + $query = $xpath->query($query); + $value = null; + + if($query->length > 0) { + $item = $query->item(0); + + if($item->hasAttributes()) { + $value = $item->attributes->getNamedItem($namedAttr); + + if(strlen($value->nodeValue) > 0) { + $value = $value->nodeValue; + } + } + } + + return $value; + } + + /** + * @param DOMXPath $xpath The DOMXPath object to carry out the query on + * @param string $query The XPath query to find the relevant node + * @return string|null Either the first matching node's value (there should only ever be one), or null if none found + */ + private function getNodeValue(DOMXPath $xpath, $query) + { + $query = $xpath->query($query); + return ($query->length > 0 ? $query->item(0)->nodeValue : null); + } +} diff --git a/code/model/RealMeUser.php b/code/model/RealMeUser.php new file mode 100644 index 0000000..5ab7054 --- /dev/null +++ b/code/model/RealMeUser.php @@ -0,0 +1,65 @@ +SPNameID) && is_string($this->SessionIndex); + if (Config::inst()->get("RealMeService", "integration_type") === RealMeService::TYPE_LOGIN) { + return $validLogin; + } + + // Federated login requires the FIT. + $hasFederatedLogin = $validLogin && is_string($this->UserFederatedTag) && $this->Attributes instanceof ArrayData; + if ($hasFederatedLogin && $this->getFederatedIdentity()) { + return $this->getFederatedIdentity()->isValid(); + } + + return false; + } + + /** + * Alias of isValid(), but called this way so it's clear that a valid RealMeUser object is semantically the same as + * an authenticated user + * @return bool + */ + public function isAuthenticated() + { + return $this->isValid(); + } + + /** + * @return RealMeFederatedIdentity|null + */ + public function getFederatedIdentity() + { + // Check if identity is present + if(!array_key_exists('FederatedIdentity', $this->array)) { + return null; + } + + // Get federated identity from array + $id = $this->array['FederatedIdentity']; + + // Sanity check class + if(!$id instanceof RealMeFederatedIdentity) { + return null; + } + + return $id; + } +} diff --git a/code/tasks/RealMeSetupTask.php b/code/tasks/RealMeSetupTask.php index d747f90..cbc69a3 100644 --- a/code/tasks/RealMeSetupTask.php +++ b/code/tasks/RealMeSetupTask.php @@ -9,9 +9,6 @@ * - Check to ensure that the task is being run from the cmdline (not in the browser, it's too sensitive) * - Check to ensure that the task hasn't already been run, and if it has, fail unless `force=1` is passed to the script * - Validate all required values have been added in the appropriate place, and provide appropriate errors if not - * - Create config.php file for simpleSAMLphp to consume, and write it in the appropriate place - * - Create authsources.php file for simpleSAMLphp to consume, and write it to the appropriate place - * - Create saml20-idp-remote.php file for simpleSAMLphp to consume, and write it to the appropriate place * - Output metadata XML that must be submitted to RealMe in order to integrate with ITE and Production environments */ class RealMeSetupTask extends BuildTask @@ -48,19 +45,10 @@ public function run($request) } // Validate all required values exist - $forceRun = ($request->getVar('force') == 1); $forEnv = $request->getVar('forEnv'); // Throws an exception if there was a problem with the config. - $this->validateInputs($forceRun, $forEnv); - - $this->createConfigReadmeFromTemplate(); - - $this->createConfigFromTemplate(); - - $this->createAuthSourcesFromTemplate(); - - $this->createMetadataFromTemplate(); + $this->validateInputs($forEnv); $this->outputMetadataXmlContent($forEnv); @@ -74,29 +62,18 @@ public function run($request) * Validate all inputs to this setup script. Ensures that all required values are available, where-ever they need to * be loaded from (environment variables, Config API, or directly passed to this script via the cmd-line) * - * @param bool $forceRun Whether or not to force the setup (therefore skip checks around existing files) - * @param string $forEnv The environment that we want to output content for (mts, ite, or prod) + * @param string $forEnv The environment that we want to output content for (mts, ite, or prod) * * @throws Exception if there were errors with the request or setup format. */ - private function validateInputs($forceRun, $forEnv) + private function validateInputs($forEnv) { - - // Ensure we haven't already run before, or if we have, that force=1 is passed - $this->validateRunOnce($forceRun); - // Ensure that 'forEnv=' is specified on the cli, and ensure that it matches a RealMe environment $this->validateRealMeEnvironments($forEnv); - // Ensure we have a config directory and that it's writeable by the web server - $this->validateSimpleSamlConfig(); - // Ensure we have the necessary directory structures, and their visibility $this->validateDirectoryStructure(); - // Make sure we can create salts and passwords using the required libraries - $this->validateCryptographicLibraries(); - // Ensure we have the certificates in the correct places. $this->validateCertificates(); @@ -104,10 +81,7 @@ private function validateInputs($forceRun, $forEnv) $this->validateEntityID(); // Make sure we have an authncontext for each environment. - $this->validateAuthnContext(); - - // Ensure the consumer URL is correct - $this->validateConsumerAssertionURL($forEnv); + $this->validateAuthNContext(); // Ensure data required for metadata XML output exists $this->validateMetadata(); @@ -130,142 +104,6 @@ private function validateInputs($forceRun, $forEnv) $this->message(_t('RealMeSetupTask.VALIDATION_SUCCESS')); } - private function createConfigReadmeFromTemplate() - { - // Create configuration files - $this->message(sprintf( - 'Creating README file in %s from template dir %s', - $this->service->getSimpleSamlConfigDir(), - $this->getConfigurationTemplateDir() - )); - - $configDir = $this->getConfigurationTemplateDir(); - $templateFile = Controller::join_links($configDir, 'README.md'); - - if (false === $this->isReadable($templateFile)) { - throw new Exception(sprintf("Can't read README.md file at %s", $templateFile)); - } - - $this->writeConfigFile($templateFile, $this->getSimpleSAMLConfigReadmeFilePath()); - } - - /** - * Create primary configuration file and place in SimpleSAMLphp configuration directory - */ - private function createConfigFromTemplate() - { - $this->message(sprintf( - 'Creating config file in %s/config/ from config in template dir %s', - $this->service->getSimpleSamlConfigDir(), - $this->getConfigurationTemplateDir() - )); - - $configDir = $this->getConfigurationTemplateDir(); - $templateFile = Controller::join_links($configDir, 'config.php'); - - if (false === $this->isReadable($templateFile)) { - throw new Exception(sprintf("Can't read config.php file at %s", $templateFile)); - } - - $this->writeConfigFile( - $templateFile, - $this->getSimpleSAMLConfigFilePath(), - array( - '{{baseurlpath}}' => $this->service->getSimpleSamlBaseUrlPath(), - '{{certdir}}' => $this->service->getCertDir(), - '{{loggingdir}}' => $this->service->getLoggingDir(), - '{{tempdir}}' => $this->service->getTempDir(), - '{{metadatadir}}' => $this->service->getSimpleSamlMetadataDir(), - '{{adminpassword}}' => $this->service->findOrMakeSimpleSAMLPassword(), - '{{secretsalt}}' => $this->service->generateSimpleSAMLSalt(), - ) - ); - } - - /** - * Create authentication sources configuration file and place in SimpleSAMLphp configuration directory - */ - private function createAuthSourcesFromTemplate() - { - $this->message(sprintf( - 'Creating authsources file in %s/config/ from config in template dir %s', - $this->service->getSimpleSamlConfigDir(), - $this->getConfigurationTemplateDir() - )); - - $configDir = $this->getConfigurationTemplateDir(); - - $templateFile = Controller::join_links($configDir, 'authsources.php'); - - if (false === $this->isReadable($templateFile)) { - throw new Exception(sprintf("Can't read authsources.php file at %s", $templateFile)); - } - - /** - * @todo Determine what to do with multiple certificates. - * - * This currently uses the same signing and mutual certificate paths and password for all 3 environments. This - * means that you can't test e.g. connectivity with ITE on the production server environment. However, the - * alternative is that all certificates and passwords must be present on all servers, which is sub-optimal. - * - * See realme/templates/simplesaml-configuration/authsources.php - */ - $this->writeConfigFile( - $templateFile, - $this->getSimpleSAMLAuthSourcesFilePath(), - array( - '{{mts-entityID}}' => $this->service->getEntityIDForEnvironment('mts'), - '{{mts-authncontext}}' => $this->service->getAuthnContextForEnvironment('mts'), - '{{mts-privatepemfile-signing}}' => $this->service->getSigningCertPath(), - '{{mts-privatepemfile-mutual}}' => $this->service->getMutualCertPath(), - '{{mts-privatepemfile-signing-password}}' => $this->service->getSigningCertPassword(), - '{{mts-privatepemfile-mutual-password}}' => $this->service->getMutualCertPassword(), - '{{mts-backchannel-proxyhost}}' => $this->service->getProxyHostForEnvironment('mts'), - '{{mts-backchannel-proxyport}}' => $this->service->getProxyPortForEnvironment('mts'), - '{{ite-entityID}}' => $this->service->getEntityIDForEnvironment('ite'), - '{{ite-authncontext}}' => $this->service->getAuthnContextForEnvironment('ite'), - '{{ite-privatepemfile-signing}}' => $this->service->getSigningCertPath(), - '{{ite-privatepemfile-mutual}}' => $this->service->getMutualCertPath(), - '{{ite-privatepemfile-signing-password}}' => $this->service->getSigningCertPassword(), - '{{ite-privatepemfile-mutual-password}}' => $this->service->getMutualCertPassword(), - '{{ite-backchannel-proxyhost}}' => $this->service->getProxyHostForEnvironment('ite'), - '{{ite-backchannel-proxyport}}' => $this->service->getProxyPortForEnvironment('ite'), - '{{prod-entityID}}' => $this->service->getEntityIDForEnvironment('prod'), - '{{prod-authncontext}}' => $this->service->getAuthnContextForEnvironment('prod'), - '{{prod-privatepemfile-signing}}' => $this->service->getSigningCertPath(), - '{{prod-privatepemfile-mutual}}' => $this->service->getMutualCertPath(), - '{{prod-privatepemfile-signing-password}}' => $this->service->getSigningCertPassword(), - '{{prod-privatepemfile-mutual-password}}' => $this->service->getMutualCertPassword(), - '{{prod-backchannel-proxyhost}}' => $this->service->getProxyHostForEnvironment('prod'), - '{{prod-backchannel-proxyport}}' => $this->service->getProxyPortForEnvironment('prod'), - ) - ); - } - - /** - * Create metadata configuration file and place in SimpleSAMLphp configuration directory - */ - private function createMetadataFromTemplate() - { - $this->message(sprintf( - 'Creating saml20-idp-remote file in %s/metadata/ from config in template dir %s', - $this->service->getSimpleSamlConfigDir(), - $this->getConfigurationTemplateDir()) - ); - - $configDir = $this->getConfigurationTemplateDir(); - $templateFile = Controller::join_links($configDir, 'saml20-idp-remote.php'); - - if (false === $this->isReadable($templateFile)) { - throw new Exception(sprintf("Can't read saml20-idp-remote.php file at %s", $templateFile)); - } - - $this->writeConfigFile( - $templateFile, - $this->getSimpleSAMLMetadataFilePath() - ); - } - /** * Outputs metadata template XML to console, so it can be sent to RealMe Operations team * @@ -292,9 +130,10 @@ private function outputMetadataXmlContent($forEnv) $message = $this->replaceTemplateContents( $templateFile, array( - '{{entityID}}' => $this->service->getEntityIDForEnvironment($forEnv), - '{{certificate-data}}' => $this->service->getSigningCertContent(), - '{{assertion-service-url}}' => $this->service->getAssertionConsumerServiceUrlForEnvironment($forEnv), + '{{entityID}}' => $this->service->getSPEntityID(), + '{{certificate-data}}' => $this->service->getSPCertContent(), + '{{nameidformat}}' => $this->service->getNameIdFormat(), + '{{acs-url}}' => $this->service->getAssertionConsumerServiceUrlForEnvironment($forEnv), '{{organisation-name}}' => $this->service->getMetadataOrganisationName(), '{{organisation-display-name}}' => $this->service->getMetadataOrganisationDisplayName(), '{{organisation-url}}' => $this->service->getMetadataOrganisationUrl(), @@ -307,31 +146,6 @@ private function outputMetadataXmlContent($forEnv) $this->message($message); } - /** - * Writes configuration from a template file with {{variables}} to its final location for SimpleSAMLphp - * - * @param string $templatePath The path to the template file - * @param string $newFilePath The path where the new file will be written - * @param array|null $replacements An array of '{{variable}}' => 'value' replacements - */ - private function writeConfigFile($templatePath, $newFilePath, $replacements = null) - { - $configText = $this->replaceTemplateContents($templatePath, $replacements); - - // If the parent folder of $newFilePath doesn't already exist, then create it - // Specifically only look one level higher, we already validate that everything else exists and can be written - $fileParentDir = dirname($newFilePath); - if (false === is_dir($fileParentDir)) { - mkdir($fileParentDir, 0744); - } - - if (false === file_put_contents($newFilePath, $configText)) { - throw new Exception( - sprintf("Could not write template file '%s' to location '%s'", $templatePath, $newFilePath) - ); - } - } - /** * Replace content in a template file with an array of replacements * @@ -350,46 +164,6 @@ private function replaceTemplateContents($templatePath, $replacements = null) return $configText; } - /** - * @return string The path to the README file we create to help identify this configuration directory - */ - private function getSimpleSAMLConfigReadmeFilePath() - { - return sprintf('%s/README.md', $this->service->getSimpleSamlConfigDir()); - } - - /** - * @return string The path to the main SimpleSAMLphp configuration file, once written - */ - private function getSimpleSAMLConfigFilePath() - { - return sprintf('%s/config.php', $this->service->getSimpleSamlConfigDir()); - } - - /** - * @return string The path to the authentication sources configuration file, once written - */ - private function getSimpleSAMLAuthSourcesFilePath() - { - return sprintf('%s/authsources.php', $this->service->getSimpleSamlConfigDir()); - } - - /** - * @return string The path to the metadata configuration file, once written - */ - private function getSimpleSAMLMetadataFilePath() - { - return sprintf('%s/metadata/saml20-idp-remote.php', $this->service->getSimpleSamlConfigDir()); - } - - /** - * @return string The path from the server root to the physical location where SimpleSAMLphp is installed - */ - private function getSimpleSAMLVendorPath() - { - return sprintf('%s/vendor/madmatt/simplesamlphp', BASE_PATH); - } - /** * @return string The full path to RealMe configuration */ @@ -402,7 +176,7 @@ private function getConfigurationTemplateDir() return $path; } - return Controller::join_links(BASE_PATH, REALME_MODULE_PATH . '/templates/simplesaml-configuration'); + return Controller::join_links(BASE_PATH, REALME_MODULE_PATH . '/templates/saml-conf'); } /** @@ -426,17 +200,6 @@ private function isReadable($filename) return is_readable($filename); } - /** - * Thin wrapper around is_writeable(), used mainly so we can test this class completely - * - * @param string $filename The filename or directory to test - * @return bool true if the file/dir is writeable, false if not - */ - private function isWriteable($filename) - { - return is_writeable($filename); - } - /** * The entity ID will pass validation, but raise an exception if the format of the service name and privacy realm * are in the incorrect format. @@ -447,68 +210,63 @@ private function isWriteable($filename) */ private function validateEntityID() { - foreach ($this->service->getAllowedRealMeEnvironments() as $env) { - $entityId = $this->service->getEntityIDForEnvironment($env); + $entityId = $this->service->getSPEntityID(); - if (true === is_null($entityId)) { - $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_NO_ENTITYID', '', '', array('env' => $env)); - } + if (is_null($entityId)) { + $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_NO_ENTITYID', '', '', array('env' => $env)); + } - // make sure the entityID is a valid URL - $entityId = filter_var($entityId, FILTER_VALIDATE_URL); - if (false === $entityId) { - $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID', '', '', - array( - 'env' => $env, - 'entityId' => $entityId - ) - ); - - // invalid entity id, no point continuing. - return; - } + // make sure the entityID is a valid URL + $entityId = filter_var($entityId, FILTER_VALIDATE_URL); + if ($entityId === false) { + $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID', '', '', + array( + 'entityId' => $entityId + ) + ); - // check it's not localhost and HTTPS. and make sure we have a host / scheme - $urlParts = parse_url($entityId); - if ('localhost' === $urlParts['host'] || 'http' === $urlParts['scheme']) { - $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID', '', '', - array( - 'env' => $env, - 'entityId' => $entityId - ) - ); - // if there's this much wrong, we want them to fix it first. - return; - } + // invalid entity id, no point continuing. + return; + } - $path = ltrim($urlParts['path']); - $urlParts = preg_split("/\\//", $path); - - - // "https://www.domain.govt.nz//" - // Validate Service Name - $serviceName = array_pop($urlParts); - if (mb_strlen($serviceName) > 10 || 0 === mb_strlen($serviceName)) { - $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID_SERVICE_NAME', '', '', - array( - 'env' => $env, - 'serviceName' => $serviceName, - 'entityId' => $entityId - ) - ); - } + // check it's not localhost and HTTPS. and make sure we have a host / scheme + $urlParts = parse_url($entityId); + if ($urlParts['host'] === 'localhost' || $urlParts['scheme'] === 'http') { + $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID', '', '', + array( + 'entityId' => $entityId + ) + ); - // Validate Privacy Realm - $privacyRealm = array_pop($urlParts); - if (null === $privacyRealm || 0 === mb_strlen($privacyRealm)) { - $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID_PRIVACY_REALM', '', '', - array( - 'env' => $env, - 'privacyRealm' => $privacyRealm, - 'entityId' => $entityId - ) - ); - } + // if there's this much wrong, we want them to fix it first. + return; + } + + $path = ltrim($urlParts['path']); + $urlParts = preg_split("/\\//", $path); + + + // A valid Entity ID is in the form of "https://www.domain.govt.nz//" + // Validate Service Name + $serviceName = array_pop($urlParts); + if (mb_strlen($serviceName) > 20 || 0 === mb_strlen($serviceName)) { + $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID_SERVICE_NAME', '', '', + array( + 'serviceName' => $serviceName, + 'entityId' => $entityId + ) + ); + } + + // Validate Privacy Realm + $privacyRealm = array_pop($urlParts); + if (null === $privacyRealm || 0 === mb_strlen($privacyRealm)) { + $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ENTITYID_PRIVACY_REALM', '', '', + array( + 'privacyRealm' => $privacyRealm, + 'entityId' => $entityId + ) + ); } } @@ -521,42 +279,20 @@ private function validateAuthNContext() { foreach ($this->service->getAllowedRealMeEnvironments() as $env) { $context = $this->service->getAuthnContextForEnvironment($env); - if (true === is_null($context)) { + if (is_null($context)) { $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_NO_AUTHNCONTEXT', '', '', array('env' => $env)); } - if (false === in_array($context, $this->service->getAllowedAuthNContextList())) { + if (!in_array($context, $this->service->getAllowedAuthNContextList())) { $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_INVALID_AUTHNCONTEXT', '', '', array('env' => $env)); } } } - /** - * Validate that this script has only been run once. It must be deliberate to overwrite the configuration settings - * as this could potentially change the privacy realms and the associated FLT. You will loose context and the - * matching of users to FLTs if this is the case. - * - * @param $forceRun boolean - */ - private function validateRunOnce($forceRun) - { - $existingFiles = array( - $this->getSimpleSAMLConfigFilePath(), - $this->getSimpleSAMLAuthSourcesFilePath(), - $this->getSimpleSAMLMetadataFilePath() - ); - - foreach ($existingFiles as $filePath) { - if (true === file_exists($filePath) && false === $forceRun) { - $this->errors[] = _t('RealMeSetupTask.ERR_ALREADY_RUN', '', '', array('path' => $filePath)); - } - } - } - /** * Ensure's the environment we're building the setup for exists. * - * @param $forEnv string + * @param string $forEnv The environment that we're going to configure with this run. */ private function validateRealMeEnvironments($forEnv) { @@ -586,38 +322,14 @@ private function validateRealMeEnvironments($forEnv) } } - /** - * Validates the SimpleSaml Config directories and ensures this script can write to them. Note: it's important that - * this script is run by the web user as this will be the user accessing the files, and writing to the log. - * - * @return array - */ - private function validateSimpleSamlConfig() - { - if (true === is_null($this->service->getSimpleSamlConfigDir())) { - $this->errors[] = _t('RealMeSetupTask.ERR_SIMPLE_SAML_CONFIG_DIR_MISSING'); - } elseif (false === $this->isWriteable($this->service->getSimpleSamlConfigDir())) { - $this->errors[] = _t( - 'RealMeSetupTask.ERR_SIMPLE_SAML_CONFIG_DIR_NOT_WRITEABLE', - '', - '', - array('dir' => $this->service->getSimpleSamlConfigDir()) - ); - } - - if (true === is_null($this->service->getSimpleSamlBaseUrlPath())) { - $this->errors[] = _t('RealMeSetupTask.ERR_BASE_DIR_MISSING'); - } - } - /** * Ensures that the directory structure is correct and the necessary directories are writable. */ private function validateDirectoryStructure() { - if (true === is_null($this->service->getCertDir())) { + if (is_null($this->service->getCertDir())) { $this->errors[] = _t('RealMeSetupTask.ERR_CERT_DIR_MISSING'); - } elseif (false === $this->isReadable($this->service->getCertDir())) { + } elseif (!$this->isReadable($this->service->getCertDir())) { $this->errors[] = _t( 'RealMeSetupTask.ERR_CERT_DIR_NOT_READABLE', '', @@ -625,31 +337,6 @@ private function validateDirectoryStructure() array('dir' => $this->service->getCertDir()) ); } - - if (true === is_null($this->service->getLoggingDir())) { - $this->errors[] = _t('RealMeSetupTask.ERR_LOG_DIR_MISSING'); - } elseif (false === $this->isWriteable($this->service->getLoggingDir())) { - $this->errors[] = _t( - 'RealMeSetupTask.ERR_LOG_DIR_NOT_WRITEABLE', - '', - '', - array('dir' => $this->service->getLoggingDir()) - ); - } - - if (true === is_null($this->service->getTempDir())) { - $this->errors[] = _t('RealMeSetupTask.ERR_TEMP_DIR_MISSING'); - } elseif ( - false === $this->isWriteable($this->service->getTempDir()) - && false === $this->isWriteable(dirname($this->service->getTempDir())) - ) { - $this->errors[] = _t( - 'RealMeSetupTask.ERR_TEMP_DIR_NOT_WRITEABLE', - '', - '', - array('dir' => $this->service->getTempDir()) - ); - } } /** @@ -657,20 +344,20 @@ private function validateDirectoryStructure() */ private function validateMetadata() { - if (true === is_null($this->service->getMetadataOrganisationName())) { + if (is_null($this->service->getMetadataOrganisationName())) { $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_NO_ORGANISATION_NAME'); } - if (true === is_null($this->service->getMetadataOrganisationDisplayName())) { + if (is_null($this->service->getMetadataOrganisationDisplayName())) { $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_NO_ORGANISATION_DISPLAY_NAME'); } - if (true === is_null($this->service->getMetadataOrganisationUrl())) { + if (is_null($this->service->getMetadataOrganisationUrl())) { $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_NO_ORGANISATION_URL'); } $contact = $this->service->getMetadataContactSupport(); - if (true === is_null($contact['company']) || true === is_null($contact['firstNames']) || is_null($contact['surname'])) { + if (is_null($contact['company']) || is_null($contact['firstNames']) || is_null($contact['surname'])) { $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_NO_SUPPORT_CONTACT'); } } @@ -681,7 +368,7 @@ private function validateMetadata() private function validateCertificates() { $signingCertFile = $this->service->getSigningCertPath(); - if (true === is_null($signingCertFile) || false === $this->isReadable($signingCertFile)) { + if (is_null($signingCertFile) || !$this->isReadable($signingCertFile)) { $this->errors[] = _t( 'RealMeSetupTask.ERR_CERT_NO_SIGNING_CERT', '', @@ -690,7 +377,7 @@ private function validateCertificates() 'const' => 'REALME_SIGNING_CERT_FILENAME' ) ); - } elseif (true === is_null($this->service->getSigningCertContent())) { + } elseif (is_null($this->service->getSPCertContent())) { // Signing cert exists, but doesn't include BEGIN/END CERTIFICATE lines, or doesn't contain the cert $this->errors[] = _t( 'RealMeSetupTask.ERR_CERT_SIGNING_CERT_CONTENT', @@ -699,54 +386,5 @@ private function validateCertificates() array('file' => $this->service->getSigningCertPath()) ); } - - $mutualCertFile = $this->service->getMutualCertPath(); - if (true === is_null($mutualCertFile) || false === $this->isReadable($mutualCertFile)) { - $this->errors[] = _t( - 'RealMeSetupTask.ERR_CERT_NO_MUTUAL_CERT', - '', - '', - array( - 'const' => 'REALME_MUTUAL_CERT_FILENAME' - ) - ); - } - } - - /** - * Ensures the server has the correct cryptographic libraries installed by trying to generate salts and passwords - * using these libraries - */ - private function validateCryptographicLibraries() - { - if (true === is_null($this->service->findOrMakeSimpleSAMLPassword())) { - $this->errors[] = _t('RealMeSetupTask.ERR_SIMPLE_SAML_NO_ADMIN_PASS'); - } - - if (true === is_null($this->service->generateSimpleSAMLSalt())) { - $this->errors[] = _t('RealMeSetupTask.ERR_SIMPLE_SAML_NO_SALT'); - } - } - - /** - * Ensure the consumerAssertionUrl is correct for this environment - * - * @param $forEnv - */ - private function validateConsumerAssertionURL($forEnv) - { - // Ensure the assertion consumer service location exists - $consumerAssertionUrl = $this->service->getAssertionConsumerServiceUrlForEnvironment($forEnv); - if (null === $consumerAssertionUrl) { - $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ASSERTION_SERVICE_URL', '', '', array('env' => $forEnv)); - - // no point in validating an invalid/missing url. - return; - } - - $urlParts = parse_url($consumerAssertionUrl); - if ('localhost' === $urlParts['host'] || 'http' === $urlParts['scheme']) { - $this->errors[] = _t('RealMeSetupTask.ERR_CONFIG_ASSERTION_SERVICE_URL', '', '', array('env' => $forEnv)); - } } } diff --git a/composer.json b/composer.json index 8dd32c1..ab92373 100644 --- a/composer.json +++ b/composer.json @@ -18,15 +18,12 @@ "require": { "composer/installers": "*", "silverstripe/framework": "~3.1", - "madmatt/simplesamlphp": "dev-ss-master" + "onelogin/php-saml": "~2.10" }, "require-dev": { - "hafriedlander/silverstripe-phockito": "*" + "phpunit/phpunit": "~3.7" }, "support": { "issues": "https://gitlab.cwp.govt.nz/silverstripe/realme/issues" - }, - "autoload": { - "files": [ "bootstrap.php" ] } } diff --git a/css/realme.css b/css/realme.css index ff2bb43..176cc55 100755 --- a/css/realme.css +++ b/css/realme.css @@ -77,8 +77,8 @@ } /* - The base RealMe widget - ----------------------------------------------------- + The base RealMe widget + ----------------------------------------------------- */ .realme_widget { padding: 0.76923em 1.07692em; @@ -108,8 +108,8 @@ } /* - Typography - ----------------------------------------------------- + Typography + ----------------------------------------------------- */ .realme_title { margin-top: 0; @@ -124,8 +124,8 @@ } /* - Buttons - ----------------------------------------------------- + Buttons + ----------------------------------------------------- */ .realme_button { line-height: 1; @@ -150,6 +150,14 @@ border-color: #1d5794; -webkit-appearance: none; -webkit-border-image: none; + + /** + * Added for SilverStripe module: + * SilverStripe uses