RealMeService   F
last analyzed

Complexity

Total Complexity 134

Size/Duplication

Total Lines 1202
Duplicated Lines 0 %

Importance

Changes 9
Bugs 2 Features 0
Metric Value
wmc 134
eloc 461
c 9
b 2
f 0
dl 0
loc 1202
rs 2

42 Methods

Rating   Name   Duplication   Size   Complexity  
A get_template_global_variables() 0 5 1
B getAuthData() 0 58 8
A processSamlErrors() 0 28 6
A getErrorBackURL() 0 11 2
A getBackURL() 0 11 2
A getCertDir() 0 11 2
A getSigningCertPath() 0 3 1
A clearLogin() 0 10 1
A getAuthnContextForEnvironment() 0 3 1
A getIdPCertContent() 0 3 1
A currentRealMeUser() 0 3 1
A current_realme_user() 0 8 3
A getUserData() 0 3 1
A isAuthenticated() 0 4 2
B enforceLogin() 0 47 8
A getLastError() 0 3 1
A validSiteURL() 0 10 3
A getIdPCertPath() 0 6 1
A getSPCertContent() 0 3 1
A getNameIdFormat() 0 11 3
A getMetadataAssertionServiceDomainForEnvironment() 0 3 1
A getSPEntityID() 0 3 1
A retrieveFederatedIdentityTag() 0 10 2
A getAssertionConsumerServiceUrlForEnvironment() 0 13 3
A retrieveFederatedLogonTag() 0 3 1
A getMetadataOrganisationUrl() 0 4 2
A getIdPEntityID() 0 4 1
A getMetadataContactSupport() 0 10 4
A getSingleSignOnServiceURL() 0 4 1
A getAllowedRealMeEnvironments() 0 3 1
B getConfigurationVarByEnv() 0 33 9
A getCertPath() 0 17 5
A getMetadataOrganisationDisplayName() 0 4 2
A getMetadataOrganisationName() 0 4 2
A getAllowedAuthNContextList() 0 3 1
B getCertificateContents() 0 36 9
A getRequest() 0 7 2
A getRequestedAuthnContext() 0 3 1
A user_data() 0 28 6
B getAuth() 0 72 7
C findErrorMessageForCode() 0 102 13
B retrieveFederatedIdentity() 0 54 11

How to fix   Complexity   

Complex Class

Complex classes like RealMeService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RealMeService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\RealMe;
4
5
use DOMDocument;
6
use DOMNodeList;
7
use Exception as BaseException;
8
use InvalidArgumentException;
9
use OneLogin\Saml2\Auth;
10
use OneLogin\Saml2\Error;
11
use OneLogin\Saml2\Response;
12
use OneLogin\Saml2\Utils;
13
use Psr\Log\LoggerInterface;
14
use SilverStripe\Control\Controller;
15
use SilverStripe\Control\Director;
16
use SilverStripe\Control\HTTPRequest;
17
use SilverStripe\Core\Config\Configurable;
18
use SilverStripe\Core\Environment;
19
use SilverStripe\Core\Injector\Injectable;
20
use SilverStripe\Core\Injector\Injector;
21
use SilverStripe\RealMe\Exception as RealMeException;
22
use SilverStripe\RealMe\Model\FederatedIdentity;
23
use SilverStripe\RealMe\Model\User;
24
use SilverStripe\Security\Member;
25
use SilverStripe\Security\Security;
26
use SilverStripe\View\TemplateGlobalProvider;
27
28
class RealMeService implements TemplateGlobalProvider
29
{
30
    use Configurable, Injectable;
31
32
    /**
33
     * Current RealMe supported environments.
34
     */
35
    const ENV_MTS = 'mts';
36
    const ENV_ITE = 'ite';
37
    const ENV_PROD = 'prod';
38
39
    /**
40
     * SAML binding types
41
     */
42
    const TYPE_LOGIN = 'login';
43
    const TYPE_ASSERT = 'assert';
44
45
    /**
46
     * the valid AuthN context values for each supported RealMe environment.
47
     */
48
    const AUTHN_LOW_STRENGTH = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:LowStrength';
49
    const AUTHN_MOD_STRENTH = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength';
50
    const AUTHN_MOD_MOBILE_SMS =
51
        'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Mobile:SMS';
52
    const AUTHN_MOD_TOKEN_SID =
53
        'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Token:SID';
54
55
    /**
56
     * Realme SAML2 error status constants
57
     */
58
    const ERR_TIMEOUT                = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:status:Timeout';
59
    const ERR_INTERNAL_ERROR         = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:status:InternalError';
60
61
    /**
62
     * SAML2 Error constants used for business logic and switching error messages
63
     */
64
    const ERR_AUTHN_FAILED           = 'urn:oasis:names:tc:SAML:2.0:status:AuthnFailed';
65
    const ERR_UNKNOWN_PRINCIPAL      = 'urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal';
66
    const ERR_NO_AVAILABLE_IDP       = 'urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP';
67
    const ERR_NO_PASSIVE             = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive';
68
    const ERR_NO_AUTHN_CONTEXT       = 'urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext';
69
    const ERR_REQUEST_UNSUPPORTED    = 'urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported';
70
    const ERR_REQUEST_DENIED         = 'urn:oasis:names:tc:SAML:2.0:status:RequestDenied';
71
    const ERR_UNSUPPORTED_BINDING    = 'urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding';
72
73
    /**
74
     * @var bool true to sync RealMe data and create/update local {@link Member} objects upon successful authentication
75
     * @config
76
     */
77
    private static $sync_with_local_member_database = false;
78
79
    /**
80
     * @var User|null User data returned by RealMe. Provided by {@link self::ensureLogin()}.
81
     *
82
     * Data within this ArrayData is as follows:
83
     * - NameID:       ArrayData   Includes the UserFlt and associated formatting information
84
     * - UserFlt:      string      RealMe pseudonymous username / identity
85
     * - Attributes:   ArrayData   User attributes returned by RealMe
86
     * - Expire:       SS_Datetime The expiry date & time of this authentication session
87
     * - SessionIndex: string      Unique identifier used to identify a user with both IdP and SP for given user.
88
     */
89
    private static $user_data = null;
90
91
    /**
92
     * @config
93
     * @var string The RealMe environment to connect to and authenticate against. This should be set by Config, and
94
     * generally be different per SilverStripe environment (e.g. developer environments would generally use 'mts',
95
     * UAT/staging sites might use 'ite', and production sites would use 'prod'.
96
     *
97
     * Valid options:
98
     * - mts
99
     * - ite
100
     * - prod
101
     */
102
    private static $realme_env = 'mts';
103
104
    /**
105
     * @var array The RealMe environments that can be configured for use with this module.
106
     */
107
    private static $allowed_realme_environments = array(self::ENV_MTS, self::ENV_ITE, self::ENV_PROD);
108
109
    /**
110
     * @config
111
     * @var string The RealMe integration type to use when connecting to RealMe. After successful authentication:
112
     * - 'login' provides a unique FLT (Federated Login Token) back
113
     * - 'assert' provides a unique FIT (Federated Identity Token) and a {@link RealMeFederatedIdentity} object back
114
     */
115
    private static $integration_type = 'login';
116
117
    private static $allowed_realme_integration_types = array(self::TYPE_LOGIN, self::TYPE_ASSERT);
118
119
    /**
120
     * @config
121
     * @var array Stores the entity ID value for each supported RealMe environment. This needs to be setup prior to
122
     * running the `RealMeSetupTask` build task. For more information, see the module documentation. An entity ID takes
123
     * the form of a URL, e.g. https://www.agency.govt.nz/privacy-realm-name/application-name
124
     */
125
    private static $sp_entity_ids = array(
126
        self::ENV_MTS => null,
127
        self::ENV_ITE => null,
128
        self::ENV_PROD => null
129
    );
130
131
    /**
132
     * @config
133
     * @var array Stores the default identity provider (IdP) entity IDs. These can be customised if you're using an
134
     * intermediary IdP instead of connecting to RealMe directly.
135
     */
136
    private static $idp_entity_ids = array(
137
        self::ENV_MTS => array(
138
            self::TYPE_LOGIN  => 'https://mts.realme.govt.nz/saml2',
139
            self::TYPE_ASSERT => 'https://mts.realme.govt.nz/realmemts/realmeidp',
140
        ),
141
        self::ENV_ITE => array(
142
            self::TYPE_LOGIN  => 'https://www.ite.logon.realme.govt.nz/saml2',
143
            self::TYPE_ASSERT => 'https://www.ite.account.realme.govt.nz/saml2/assertion',
144
        ),
145
        self::ENV_PROD => array(
146
            self::TYPE_LOGIN  => 'https://www.logon.realme.govt.nz/saml2',
147
            self::TYPE_ASSERT => 'https://www.account.realme.govt.nz/saml2/assertion',
148
        )
149
    );
150
151
    private static $idp_sso_service_urls = array(
152
        self::ENV_MTS => array(
153
            self::TYPE_LOGIN  => 'https://mts.realme.govt.nz/logon-mts/mtsEntryPoint',
154
            self::TYPE_ASSERT => 'https://mts.realme.govt.nz/realme-mts/validate/realme-mts-idp.xhtml'
155
        ),
156
        self::ENV_ITE => array(
157
            self::TYPE_LOGIN  => 'https://www.ite.logon.realme.govt.nz/sso/logon/metaAlias/logon/logonidp',
158
            self::TYPE_ASSERT => 'https://www.ite.assert.realme.govt.nz/sso/SSORedirect/metaAlias/assertion/realmeidp'
159
        ),
160
        self::ENV_PROD => array(
161
            self::TYPE_LOGIN  => 'https://www.logon.realme.govt.nz/sso/logon/metaAlias/logon/logonidp',
162
            self::TYPE_ASSERT => 'https://www.assert.realme.govt.nz/sso/SSORedirect/metaAlias/assertion/realmeidp'
163
        )
164
    );
165
166
    /**
167
     * @var array A list of certificate filenames for different RealMe environments and integration types. These files
168
     * must be located in the directory specified by the REALME_CERT_DIR environment variable. These filenames are the
169
     * same as the files that can be found in the RealMe Shared Workspace, within the 'Integration Bundle' ZIP files for
170
     * the different environments (MTS, ITE and Production), so you just need to extract the specific certificate file
171
     * that you need and make sure it's in place on the server in the REALME_CERT_DIR.
172
     */
173
    private static $idp_x509_cert_filenames = array(
174
        self::ENV_MTS => array(
175
            self::TYPE_LOGIN  => 'mts_login_saml_idp.cer',
176
            self::TYPE_ASSERT => 'mts_assert_saml_idp.cer'
177
        ),
178
        self::ENV_ITE => array(
179
            self::TYPE_LOGIN  => 'ite.signing.logon.realme.govt.nz.cer',
180
            self::TYPE_ASSERT => 'ite.signing.account.realme.govt.nz.cer'
181
        ),
182
        self::ENV_PROD => array(
183
            self::TYPE_LOGIN  => 'signing.logon.realme.govt.nz.cer',
184
            self::TYPE_ASSERT => 'signing.account.realme.govt.nz.cer'
185
        )
186
    );
187
188
    /**
189
     * @config
190
     * @var array Stores the AuthN context values for each supported RealMe environment. This needs to be setup prior to
191
     * running the `RealMeSetupTask` build task. For more information, see the module documentation. An AuthN context
192
     * can be one of the following:
193
     *
194
     * Username and password only:
195
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:LowStrength
196
     *
197
     * Username, password, and any moderate strength second level of authenticator (RSA token, Google Auth, SMS)
198
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength
199
     *
200
     * The following two are less often used, and shouldn't be used unless there's a specific need.
201
     *
202
     * Username, password, and only SMS 2FA token
203
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Mobile:SMS
204
     *
205
     * Username, password, and only RSA 2FA token
206
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Token:SID
207
     */
208
    private static $authn_contexts = array(
209
        self::ENV_MTS => null,
210
        self::ENV_ITE => null,
211
        self::ENV_PROD => null
212
    );
213
214
    /**
215
     * @config $allowed_authn_context_list
0 ignored issues
show
Documentation Bug introduced by
The doc comment $allowed_authn_context_list at position 0 could not be parsed: Unknown type name '$allowed_authn_context_list' at position 0 in $allowed_authn_context_list.
Loading history...
216
     * @var $allowed_authn_context_list array
217
     *
218
     * A list of the valid authn context values supported for realme.
219
     */
220
    private static $allowed_authn_context_list = array(
221
        self::AUTHN_LOW_STRENGTH,
222
        self::AUTHN_MOD_STRENTH,
223
        self::AUTHN_MOD_MOBILE_SMS,
224
        self::AUTHN_MOD_TOKEN_SID
225
    );
226
227
    /**
228
     * @config
229
     * @var array Domain names for metadata files. Used in @link RealMeSetupTask when outputting metadata XML
230
     */
231
    private static $metadata_assertion_service_domains = array(
232
        self::ENV_MTS => null,
233
        self::ENV_ITE => null,
234
        self::ENV_PROD => null
235
    );
236
237
    /**
238
     * @config
239
     * @var array A list of error messages to display if RealMe returns error statuses, instead of the default
240
     * translations (found in realme/lang/en.yml for example).
241
     */
242
    private static $realme_error_message_overrides = array(
243
        self::ERR_AUTHN_FAILED => null,
244
        self::ERR_TIMEOUT => null,
245
        self::ERR_INTERNAL_ERROR => null,
246
        self::ERR_NO_AVAILABLE_IDP => null,
247
        self::ERR_REQUEST_UNSUPPORTED => null,
248
        self::ERR_NO_PASSIVE => null,
249
        self::ERR_REQUEST_DENIED => null,
250
        self::ERR_UNSUPPORTED_BINDING => null,
251
        self::ERR_UNKNOWN_PRINCIPAL => null,
252
        self::ERR_NO_AUTHN_CONTEXT => null
253
    );
254
255
    /**
256
     * @config
257
     * @var string|null The organisation name to be used in metadata XML that is submitted to RealMe
258
     */
259
    private static $metadata_organisation_name = null;
260
261
    /**
262
     * @config
263
     * @var string|null The organisation display name to be used in metadata XML that is submitted to RealMe
264
     */
265
    private static $metadata_organisation_display_name = null;
266
267
    /**
268
     * @config
269
     * @var string|null The organisation URL to be used in metadata XML that is submitted to RealMe
270
     */
271
    private static $metadata_organisation_url = null;
272
273
    /**
274
     * @config
275
     * @var string|null The support contact's company name to be used in metadata XML that is submitted to RealMe
276
     */
277
    private static $metadata_contact_support_company = null;
278
279
    /**
280
     * @config
281
     * @var string|null The support contact's first name(s) to be used in metadata XML that is submitted to RealMe
282
     */
283
    private static $metadata_contact_support_firstnames = null;
284
285
    /**
286
     * @config
287
     * @var string|null The support contact's surname to be used in metadata XML that is submitted to RealMe
288
     */
289
    private static $metadata_contact_support_surname = null;
290
291
    /**
292
     * @var Auth|null Set by {@link getAuth()}, which creates an instance of Auth to check
293
     * authentication against
294
     */
295
    private $auth = null;
296
297
    /**
298
     * @var string|null The last error message during login enforcement
299
     */
300
    private $lastError = null;
301
302
    /**
303
     * @return array
304
     */
305
    public static function get_template_global_variables()
306
    {
307
        return array(
308
            'RealMeUser' => array(
309
                'method' => 'current_realme_user'
310
            )
311
        );
312
    }
313
314
    /**
315
     * @return HTTPRequest|null
316
     */
317
    protected static function getRequest()
318
    {
319
        if (!Injector::inst()->has(HTTPRequest::class)) {
320
            return null;
321
        };
322
323
        return Injector::inst()->get(HTTPRequest::class);
324
    }
325
326
    /**
327
     * Return the user data which was saved to session from the first RealMe
328
     * auth.
329
     * Note: Does not check authenticity or expiry of this data
330
     *
331
     * @param HTTPRequest $request
332
     * @return User
333
     */
334
    public static function user_data()
335
    {
336
        if (!is_null(static::$user_data)) {
0 ignored issues
show
Bug introduced by
Since $user_data is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $user_data to at least protected.
Loading history...
337
            return static::$user_data;
338
        }
339
340
        $request = self::getRequest();
341
342
        if (!$request) {
343
            return null;
344
        }
345
346
        $sessionData = $request->getSession()->get('RealMe.SessionData');
347
348
        // Exit point
349
        if (is_null($sessionData)) {
350
            return null;
351
        }
352
353
        // Unserialise stored data
354
        $user = unserialize($sessionData);
355
356
        if ($user == false || !$user instanceof User) {
357
            return null;
358
        }
359
360
        static::$user_data = $user;
361
        return static::$user_data;
362
    }
363
364
    public function getUserData()
365
    {
366
        return static::user_data();
367
    }
368
369
    /**
370
     * Calls available user data and checks for validity
371
     *
372
     * @return User
373
     */
374
    public static function current_realme_user()
375
    {
376
        $user = self::user_data();
377
        if ($user && !$user->isValid()) {
378
            return null;
379
        }
380
381
        return $user;
382
    }
383
384
    /**
385
     * A helpful static method that follows SilverStripe naming for Member::currentUser();
386
     *
387
     * @return User
388
     */
389
    public static function currentRealMeUser()
390
    {
391
        return self::current_realme_user();
392
    }
393
394
    /**
395
     * Enforce login via RealMe. This can be used in controllers to force users to be authenticated via RealMe (not
396
     * necessarily logged in as a {@link Member}), in the form of:
397
     * <code>
398
     * Session::set('RealMeBackURL', '/path/to/the/controller/method');
399
     * if($service->enforceLogin()) {
400
     *     // User has a valid RealMe account, $service->getAuthData() will return you their details
401
     * } else {
402
     *     // Something went wrong processing their details, show an error
403
     * }
404
     * </code>
405
     *
406
     * In cases where people are *not* authenticated with RealMe, this method will redirect them directly to RealMe.
407
     *
408
     * However, generally you want this to be an explicit process, so you should look at instead using the standard
409
     * {@link RealMeAuthenticator}.
410
     *
411
     * A return value of bool false indicates that there was a failure during the authentication process (perhaps a
412
     * communication issue, or a failure to decode the response correctly. You should handle this like you would any
413
     * other unexpected authentication error. You can use {@link getLastError()} to see if a human-readable error
414
     * message exists for display to the user.
415
     *
416
     * @param HTTPRequest $request
417
     * @param string $backUrl
418
     * @return bool|null true if the user is correctly authenticated, false if there was an error with login
419
     * @throws Error
420
     */
421
    public function enforceLogin(HTTPRequest $request, $backUrl = null)
422
    {
423
        // First, check to see if we have an existing authenticated session
424
        if ($this->isAuthenticated()) {
425
            return true;
426
        }
427
428
        $session = $request->getSession();
429
430
        if ($backUrl) {
431
            $session->set('RealMeBackURL', $this->validSiteURL($backUrl));
432
        }
433
434
        // If not, attempt to retrieve authentication data from OneLogin (in case this is called during SAML assertion)
435
        try {
436
            if (!$session->get("RealMeErrorBackURL")) {
437
                $session->set("RealMeErrorBackURL", Controller::curr()->Link("Login"));
438
            }
439
440
            $auth = $this->getAuth();
441
            $auth->processResponse();
442
443
            // if there were any errors from the SAML request, process and translate them.
444
            $errors = $auth->getErrors();
445
            if (is_array($errors) && !empty($errors)) {
446
                $this->processSamlErrors($errors);
447
                return false;
448
            }
449
450
            $authData = $this->getAuthData();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $authData is correct as $this->getAuthData() targeting SilverStripe\RealMe\RealMeService::getAuthData() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
451
452
            // If no data is found, then force login
453
            if (is_null($authData)) {
0 ignored issues
show
introduced by
The condition is_null($authData) is always true.
Loading history...
454
                throw new RealMeException('No SAML data, enforcing login', RealMeException::NOT_AUTHENTICATED);
455
            }
456
457
            // call a success method as we've successfully logged in (if it exists)
458
            Member::singleton()->extend('onRealMeLoginSuccess', $authData);
459
        } catch (BaseException $e) {
460
            Member::singleton()->extend("onRealMeLoginFailure", $e);
461
462
            // No auth data or failed to decrypt, enforce login again
463
            $this->getAuth()->login(Director::absoluteBaseURL());
464
            die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
465
        }
466
467
        return $auth->isAuthenticated();
468
    }
469
470
    /**
471
     * If there was an error returned from the saml response, process the errors
472
     *
473
     * @param $errors
474
     */
475
    private function processSamlErrors(array $errors)
476
    {
477
        $translatedMessage = null;
478
479
        // The error message returned by onelogin/php-saml is the top-level error, but we want the actual error
480
        $request = Controller::curr()->getRequest();
481
        if ($request->isPOST() && $request->postVar("SAMLResponse")) {
482
            $response = new Response($this->getAuth()->getSettings(), $request->postVar("SAMLResponse"));
483
            $internalError = Utils::query(
484
                $response->document,
485
                "/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode/@Value"
486
            );
487
488
            if ($internalError instanceof DOMNodeList && $internalError->length > 0) {
489
                $internalErrorCode = $internalError->item(0)->textContent;
490
                $translatedMessage = $this->findErrorMessageForCode($internalErrorCode);
491
            }
492
        }
493
494
        // If we found a message to display, then let's redirect to the form and display it
495
        if ($translatedMessage) {
496
            $this->lastError = $translatedMessage;
497
        }
498
499
        Injector::inst()->get(LoggerInterface::class)->info(sprintf(
500
            'onelogin/php-saml error messages: %s (%s)',
501
            join(', ', $errors),
502
            $this->getAuth()->getLastErrorReason()
503
        ));
504
    }
505
506
    /**
507
     * Checks data stored in Session to see if the user is authenticated.
508
     * @return bool true if the user is authenticated via RealMe and we can trust ->getUserData()
509
     */
510
    public function isAuthenticated()
511
    {
512
        $user = $this->getUserData();
513
        return $user instanceof User && $user->isAuthenticated();
514
    }
515
516
    /**
517
     * Returns a {@link RealMeUser} object if one can be built from the RealMe session data.
518
     *
519
     * @throws Error Passes on the SAML error if it's not indicating a lack of SAML response data
520
     * @throws RealMeException If identity information exists but couldn't be decoded, or doesn't exist
521
     * @return User|null
522
     */
523
    public function getAuthData()
524
    {
525
        // returns null if the current auth is invalid or timed out.
526
        try {
527
            // Process response and capture details
528
            $auth = $this->getAuth();
529
530
            if (!$auth->isAuthenticated()) {
531
                throw new RealMeException(
532
                    'OneLogin SAML library did not successfully authenticate, but did not return a specific error',
533
                    RealMeException::NOT_AUTHENTICATED
534
                );
535
            }
536
537
            $spNameId = $auth->getNameId();
538
            if (!is_string($spNameId)) {
0 ignored issues
show
introduced by
The condition is_string($spNameId) is always true.
Loading history...
539
                throw new RealMeException('Invalid/Missing NameID in SAML response', RealMeException::MISSING_NAMEID);
540
            }
541
542
            $sessionIndex = $auth->getSessionIndex();
543
            if (!is_string($sessionIndex)) {
0 ignored issues
show
introduced by
The condition is_string($sessionIndex) is always true.
Loading history...
544
                throw new RealMeException(
545
                    'Invalid/Missing SessionIndex value in SAML response',
546
                    RealMeException::MISSING_SESSION_INDEX
547
                );
548
            }
549
550
            $attributes = $auth->getAttributes();
551
            if (!is_array($attributes)) {
0 ignored issues
show
introduced by
The condition is_array($attributes) is always true.
Loading history...
552
                throw new RealMeException(
553
                    'Invalid/Missing attributes array in SAML response',
554
                    RealMeException::MISSING_ATTRIBUTES
555
                );
556
            }
557
558
            $federatedIdentity = $this->retrieveFederatedIdentity($auth);
559
560
            // We will have either a FLT or FIT, depending on integration type
561
            if ($this->config()->integration_type == self::TYPE_ASSERT) {
562
                $userTag = $this->retrieveFederatedIdentityTag($auth);
563
            } else {
564
                $userTag = $this->retrieveFederatedLogonTag($auth);
565
            }
566
567
            return User::create([
568
                'SPNameID' => $spNameId,
569
                'UserFederatedTag' => $userTag,
570
                'SessionIndex' => $sessionIndex,
571
                'Attributes' => $attributes,
572
                'FederatedIdentity' => $federatedIdentity,
573
            ]);
574
        } catch (Error $e) {
575
            // If the Exception code indicates there wasn't a response, we ignore it as it simply means the visitor
576
            // isn't authenticated yet. Otherwise, we re-throw the Exception
577
            if ($e->getCode() === Error::SAML_RESPONSE_NOT_FOUND) {
578
                return null;
579
            } else {
580
                throw $e;
581
            }
582
        }
583
    }
584
585
    /**
586
     * Clear the RealMe credentials from Session, called during Security->logout() overrides
587
     *
588
     * @param HTTPRequest $request
589
     * @return void
590
     */
591
    public function clearLogin(HTTPRequest $request)
592
    {
593
        $this->config()->__set('user_data', null);
594
        $session = $request->getSession();
595
596
        $session->set("RealMeBackURL", null);
597
        $session->set("RealMeErrorBackURL", null);
598
        $session->set("RealMe.SessionData", null);
599
        $session->set("RealMe.OriginalResponse", null);
600
        $session->set("RealMe.LastErrorMessage", null);
601
    }
602
603
    public function getLastError()
604
    {
605
        return $this->lastError;
606
    }
607
608
    /**
609
     * @return string A BackURL as specified originally when accessing /Security/login, for use after authentication
610
     */
611
    public function getBackURL(HTTPRequest $request)
612
    {
613
        $url = null;
614
        $session = $request->getSession();
615
616
        if ($session->get('RealMeBackURL')) {
617
            $url = $session->get('RealMeBackURL');
618
            $session->clear('RealMeBackURL'); // Ensure we don't redirect back to the same error twice
619
        }
620
621
        return $this->validSiteURL($url);
622
    }
623
624
    public function getErrorBackURL(HTTPRequest $request)
625
    {
626
        $url = null;
627
        $session = $request->getSession();
628
629
        if ($session->get('RealMeErrorBackURL')) {
630
            $url = $session->get('RealMeErrorBackURL');
631
            $session->clear('RealMeErrorBackURL'); // Ensure we don't redirect back to the same error twice
632
        }
633
634
        return $this->validSiteURL($url);
635
    }
636
637
    private function validSiteURL($url = null)
638
    {
639
        if (isset($url) && Director::is_site_url($url)) {
640
            $url = Director::absoluteURL($url);
641
        } else {
642
            // Spoofing attack or no back URL set, redirect to homepage instead of spoofing url
643
            $url = Director::absoluteBaseURL();
644
        }
645
646
        return $url;
647
    }
648
649
    /**
650
     * @param String $subdir A sub-directory where certificates may be stored for
651
     * a specific case
652
     * @return string|null Either the directory where certificates are stored,
653
     * or null if undefined
654
     */
655
    public function getCertDir($subdir = null)
656
    {
657
658
        // Trim prepended seprator to avoid absolute path
659
        $path = ltrim(ltrim($subdir, '/'), '\\');
660
661
        if ($certDir = Environment::getEnv('REALME_CERT_DIR')) {
662
            $path = $certDir . '/' . $path; // Duplicate slashes will be handled by realpath()
663
        }
664
665
        return realpath($path);
666
    }
667
668
    /**
669
     * Returns the appropriate AuthN Context, given the environment passed in. The AuthNContext may be different per
670
     * environment, and should be one of the strings as defined in the static {@link self::$authn_contexts} at the top
671
     * of this class.
672
     *
673
     * @param string $env The environment to return the AuthNContext for. Must be one of the RealMe environment names
674
     * @return string|null Returns the AuthNContext for the given $env, or null if no context exists
675
     */
676
    public function getAuthnContextForEnvironment($env)
677
    {
678
        return $this->getConfigurationVarByEnv('authn_contexts', $env);
679
    }
680
681
    /**
682
     * Returns the full path to the SAML signing certificate file, used by SimpleSAMLphp to sign all messages sent to
683
     * RealMe.
684
     *
685
     * @return string|null Either the full path to the SAML signing certificate file, or null if it doesn't exist
686
     */
687
    public function getSigningCertPath()
688
    {
689
        return $this->getCertPath('SIGNING');
690
    }
691
692
    public function getIdPCertPath()
693
    {
694
        $cfg = $this->config();
695
        $name = $this->getConfigurationVarByEnv('idp_x509_cert_filenames', $cfg->realme_env, $cfg->integration_type);
696
697
        return $this->getCertDir($name);
698
    }
699
700
    public function getSPCertContent($contentType = 'certificate')
701
    {
702
        return $this->getCertificateContents($this->getSigningCertPath(), $contentType);
703
    }
704
705
    public function getIdPCertContent()
706
    {
707
        return $this->getCertificateContents($this->getIdPCertPath());
708
    }
709
710
    /**
711
     * Returns the content of the SAML signing certificate. This is used by getAuth() and by RealMeSetupTask to produce
712
     * metadata XML files.
713
     *
714
     * @param string $certPath The filesystem path to where the certificate is stored on the filesystem
715
     * @param string $contentType Either 'certificate' or 'key', depending on which part of the file to return
716
     * @return string|null The content of the signing certificate
717
     */
718
    public function getCertificateContents($certPath, $contentType = 'certificate')
719
    {
720
        $text = null;
721
722
        if (!is_null($certPath)) {
0 ignored issues
show
introduced by
The condition is_null($certPath) is always false.
Loading history...
723
            $certificateContents = file_get_contents($certPath);
724
725
            // If the file does not contain any header information and the content type is certificate, just return it
726
            if ($contentType == 'certificate' && !preg_match('/-----BEGIN/', $certificateContents)) {
727
                $text = $certificateContents;
728
            } else {
729
                // Otherwise, inspect the file and match based on the full contents
730
                if ($contentType == 'certificate') {
731
                    $pattern = '/-----BEGIN CERTIFICATE-----[\r\n]*([^-]*)[\r\n]*-----END CERTIFICATE-----/';
732
                } elseif ($contentType == 'key') {
733
                    $pattern = '/-----BEGIN [A-Z ]*PRIVATE KEY-----[\r\n]*([^-]*)[\r\n]*'
734
                        . '-----END [A-Z ]*PRIVATE KEY-----/';
735
                } else {
736
                    throw new InvalidArgumentException('Argument contentType must be either "certificate" or "key"');
737
                }
738
739
                // This is a PEM key, and we need to extract just the certificate, stripping out the private key etc.
740
                // So we search for everything between '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----'
741
                preg_match(
742
                    $pattern,
743
                    $certificateContents,
744
                    $matches
745
                );
746
747
                if (isset($matches) && is_array($matches) && isset($matches[1])) {
748
                    $text = trim($matches[1]);
749
                }
750
            }
751
        }
752
753
        return $text;
754
    }
755
756
    /**
757
     * @param string $env The environment to return the entity ID for. Must be one of the RealMe environment names
758
     * @return string|null Either the assertion consumer service location, or null if information doesn't exist
759
     */
760
    public function getAssertionConsumerServiceUrlForEnvironment($env)
761
    {
762
        if (in_array($env, $this->getAllowedRealMeEnvironments()) === false) {
763
            return null;
764
        }
765
766
        $domain = $this->getMetadataAssertionServiceDomainForEnvironment($env);
767
        if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
768
            return null;
769
        }
770
771
        // Returns https://domain.govt.nz/Security/login/RealMe/acs
772
        return Controller::join_links($domain, Security::config()->get('login_url'), 'RealMe/acs');
773
    }
774
775
    /**
776
     * @return string|null The organisation name to be used in metadata XML output, or null if none exists
777
     */
778
    public function getMetadataOrganisationName()
779
    {
780
        $orgName = $this->config()->metadata_organisation_name;
781
        return (strlen($orgName) > 0) ? $orgName : null;
782
    }
783
784
    /**
785
     * @return string|null The organisation display name to be used in metadata XML output, or null if none exists
786
     */
787
    public function getMetadataOrganisationDisplayName()
788
    {
789
        $displayName = $this->config()->metadata_organisation_display_name;
790
        return (strlen($displayName) > 0) ? $displayName : null;
791
    }
792
793
    /**
794
     * @return string|null The organisation website URL to be used in metadata XML output, or null if none exists
795
     */
796
    public function getMetadataOrganisationUrl()
797
    {
798
        $url = $this->config()->metadata_organisation_url;
799
        return (strlen($url) > 0) ? $url: null;
800
    }
801
802
    /**
803
     * @return string[] The support contact details to be used in metadata XML output, with null values if they don't
804
     *                  exist
805
     */
806
    public function getMetadataContactSupport()
807
    {
808
        $company = $this->config()->metadata_contact_support_company;
809
        $firstNames = $this->config()->metadata_contact_support_firstnames;
810
        $surname = $this->config()->metadata_contact_support_surname;
811
812
        return array(
813
            'company' => (strlen($company) > 0) ? $company : null,
814
            'firstNames' => (strlen($firstNames) > 0) ? $firstNames : null,
815
            'surname' => (strlen($surname) > 0) ? $surname : null
816
        );
817
    }
818
819
    /**
820
     * The list of RealMe environments that can be used. By default, we allow mts, ite and production.
821
     * @return array
822
     */
823
    public function getAllowedRealMeEnvironments()
824
    {
825
        return $this->config()->allowed_realme_environments;
826
    }
827
828
    /**
829
     * The list of valid realme AuthNContexts
830
     * @return array
831
     */
832
    public function getAllowedAuthNContextList()
833
    {
834
        return $this->config()->allowed_authn_context_list;
835
    }
836
837
    /**
838
     * Returns the appropriate entity ID for RealMe, given the environment passed in. The entity ID may be different per
839
     * environment, and should be a full URL, including privacy realm and application name. For example, this may be:
840
     * https://www.agency.govt.nz/privacy-realm-name/application-name
841
     *
842
     * @return string|null Returns the entity ID for the current environment, or null if no entity ID exists
843
     */
844
    public function getSPEntityID()
845
    {
846
        return $this->getConfigurationVarByEnv('sp_entity_ids', $this->config()->realme_env);
847
    }
848
849
    private function getIdPEntityID()
850
    {
851
        $cfg = $this->config();
852
        return $this->getConfigurationVarByEnv('idp_entity_ids', $cfg->realme_env, $cfg->integration_type);
853
    }
854
855
    private function getSingleSignOnServiceURL()
856
    {
857
        $cfg = $this->config();
858
        return $this->getConfigurationVarByEnv('idp_sso_service_urls', $cfg->realme_env, $cfg->integration_type);
859
    }
860
861
    private function getRequestedAuthnContext()
862
    {
863
        return $this->getConfigurationVarByEnv('authn_contexts', $this->config()->realme_env);
864
    }
865
866
    /**
867
     * Returns the internal {@link Auth} object against which visitors are authenticated.
868
     *
869
     * @return Auth
870
     */
871
    public function getAuth(HTTPRequest $request = null)
872
    {
873
        if (isset($this->auth)) {
874
            return $this->auth;
875
        }
876
877
        if (!$request) {
878
            $request = self::getRequest();
879
            if (!$request) {
880
                throw new RealMeException('A request must be provided for session access');
881
            }
882
        }
883
884
        // Ensure onelogin is using the correct host, protocol and port incase a proxy is involved
885
        Utils::setSelfHost($request->getHeader('Host'));
886
        Utils::setSelfProtocol($request->getScheme());
887
888
        $port = null;
889
        if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) {
890
            $port = $_SERVER['HTTP_X_FORWARDED_PORT'];
891
        } elseif (isset($_SERVER['SERVER_PORT'])) {
892
            $port = $_SERVER['SERVER_PORT'];
893
        }
894
895
        if ($port) {
896
            Utils::setSelfPort($port);
897
        }
898
899
        $settings = [
900
            'strict' => true,
901
            'debug' => false,
902
903
            // Service Provider (this installation) configuration
904
            'sp' => [
905
                'entityId' => $this->getSPEntityID(),
906
                'x509cert' => $this->getSPCertContent('certificate'),
907
                'privateKey' => $this->getSPCertContent('key'),
908
909
                // According to RealMe messaging spec, must always be transient for assert; is irrelevant for login
910
                'NameIDFormat' => $this->getNameIdFormat(),
911
912
                'assertionConsumerService' => [
913
                    'url' => $this->getAssertionConsumerServiceUrlForEnvironment($this->config()->realme_env),
914
                    'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' // Always POST, not artifact binding
915
                ]
916
            ],
917
918
            // RealMe Identity Provider configuration
919
            'idp' => [
920
                'entityId' => $this->getIdPEntityID(),
921
                'x509cert' => $this->getIdPCertContent(),
922
923
                'singleSignOnService' => [
924
                    'url' => $this->getSingleSignOnServiceURL(),
925
                    'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
926
                ]
927
            ],
928
929
            'security' => [
930
                'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
931
                'authnRequestsSigned' => true,
932
                'wantAssertionsEncrypted' => true,
933
                'wantAssertionsSigned' => true,
934
935
                'requestedAuthnContext' => [
936
                    $this->getRequestedAuthnContext()
937
                ]
938
            ]
939
        ];
940
941
        $this->auth = new Auth($settings);
942
        return $this->auth;
943
    }
944
945
    /**
946
     * @return string the required NameIDFormat to be included in metadata XML, based on the requested integration type
947
     */
948
    public function getNameIdFormat()
949
    {
950
        switch ($this->config()->integration_type) {
951
            case self::TYPE_ASSERT:
952
                return 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient';
953
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
954
955
            case self::TYPE_LOGIN:
956
            default:
957
                return 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent';
958
                break;
959
        }
960
    }
961
962
    /**
963
     * @param string $cfgName The static configuration value to get. This should be an array
964
     * @param string $env The environment to return the value for. Must be one of the RealMe environment names
965
     * @param string $integrationType The integration type (login or assert), if necessary, to determine return var
966
     * @throws InvalidArgumentException If the cfgVar doesn't exist, or is malformed
967
     * @return string|null Returns the value as defined in $cfgName for the given environment, or null if none exist
968
     */
969
    private function getConfigurationVarByEnv($cfgName, $env, $integrationType = null)
970
    {
971
        $value = null;
972
973
        if (in_array($env, $this->getAllowedRealMeEnvironments())) {
974
            $values = $this->config()->$cfgName;
975
976
            if (is_array($values) && isset($values[$env])) {
977
                $value = $values[$env];
978
            }
979
        }
980
981
        // If $integrationType is specified, then $value should be an array, with the array key being the integration
982
        // type and array value being the returned variable
983
        if (!is_null($integrationType) && is_array($value) && isset($value[$integrationType])) {
984
            $value = $value[$integrationType];
985
        } elseif (!is_null($integrationType)) {
986
            // Otherwise, we are expecting an integration type, but the value is not specified that way, error out
987
            throw new InvalidArgumentException(
988
                sprintf(
989
                    'Config value %s[%s][%s] not well formed (cfg var not an array)',
990
                    $cfgName,
991
                    $env,
992
                    $integrationType
993
                )
994
            );
995
        }
996
997
        if (is_null($value)) {
998
            throw new InvalidArgumentException(sprintf('Config value %s[%s] not set', $cfgName, $env));
999
        }
1000
1001
        return $value;
1002
    }
1003
1004
    /**
1005
     * @param string $certName The certificate name, either 'SIGNING' or 'MUTUAL'
1006
     * @return string|null Either the full path to the certificate file, or null if it doesn't exist
1007
     * @see self::getSigningCertPath()
1008
     */
1009
    private function getCertPath($certName)
1010
    {
1011
        $certPath = null;
1012
1013
        if (in_array($certName, array('SIGNING', 'MUTUAL'))) {
1014
            $constName = sprintf('REALME_%s_CERT_FILENAME', strtoupper($certName));
1015
            if ($filename = Environment::getEnv($constName)) {
1016
                $certPath = $this->getCertDir($filename);
1017
            }
1018
        }
1019
1020
        // Ensure the file exists, if it doesn't then set it to null
1021
        if (!is_null($certPath) && !file_exists($certPath)) {
1022
            $certPath = null;
1023
        }
1024
1025
        return $certPath;
1026
    }
1027
1028
    /**
1029
     * @param string $env The environment to return the domain name for. Must be one of the RealMe environment names
1030
     * @return string|null Either the FQDN (e.g. https://www.realme-demo.govt.nz/) or null if none is specified
1031
     */
1032
    private function getMetadataAssertionServiceDomainForEnvironment($env)
1033
    {
1034
        return $this->getConfigurationVarByEnv('metadata_assertion_service_domains', $env);
1035
    }
1036
1037
    /**
1038
     * @param Auth $auth
1039
     * @return string|null null if there's no FLT, or a string if there is one
1040
     */
1041
    private function retrieveFederatedLogonTag(Auth $auth)
1042
    {
1043
        return $auth->getNameId(); // RealMe FLT is a synonym of NameID
1044
    }
1045
1046
    /**
1047
     * @param Auth $auth
1048
     * @return string|null null if there's not FIT, or a string if there is one
1049
     */
1050
    private function retrieveFederatedIdentityTag(Auth $auth)
1051
    {
1052
        $fit = null;
1053
        $attributes = $auth->getAttributes();
1054
1055
        if (isset($attributes['urn:nzl:govt:ict:stds:authn:attribute:igovt:IVS:FIT'])) {
1056
            $fit = $attributes['urn:nzl:govt:ict:stds:authn:attribute:igovt:IVS:FIT'][0];
1057
        }
1058
1059
        return $fit;
1060
    }
1061
1062
    /**
1063
     * @param Auth $auth
1064
     * @return FederatedIdentity|null
1065
     * @throws RealMeException
1066
     */
1067
    private function retrieveFederatedIdentity(Auth $auth)
1068
    {
1069
        $federatedIdentity = null;
1070
        $attributes = $auth->getAttributes();
1071
        $nameId = $auth->getNameId();
1072
1073
        if (!isset($attributes[FederatedIdentity::SOURCE_XML]) && !isset($attributes[FederatedIdentity::SOURCE_JSON])) {
1074
            return $federatedIdentity;
1075
        }
1076
1077
        $source = isset($attributes[FederatedIdentity::SOURCE_XML])
1078
            ? FederatedIdentity::SOURCE_XML
1079
            : FederatedIdentity::SOURCE_JSON;
1080
1081
        // Identity information is encoded using 'Base 64 Encoding with URL and Filename Safe Alphabet'
1082
        // For more info, review RFC3548, section 4 (https://tools.ietf.org/html/rfc3548#page-6)
1083
        // Note: This is different to PHP's standard base64_decode() function, therefore we need to swap chars
1084
        // to match PHP's expectations:
1085
        // char 62 (-) becomes +
1086
        // char 63 (_) becomes /
1087
1088
        $identity = $attributes[$source];
1089
1090
        if (!is_array($identity) || !isset($identity[0])) {
1091
            throw new RealMeException(
1092
                'Invalid identity response received from RealMe',
1093
                RealMeException::INVALID_IDENTITY_VALUE
1094
            );
1095
        }
1096
1097
        // Switch from filename-safe alphabet base64 encoding to standard base64 encoding
1098
        $identity = strtr($identity[0], '-_', '+/');
1099
        $identity = base64_decode($identity, true);
1100
1101
        if (is_bool($identity) && !$identity) {
0 ignored issues
show
introduced by
The condition is_bool($identity) is always false.
Loading history...
1102
            // Strict base64_decode fails, either the identity didn't exist or was mangled during transmission
1103
            throw new RealMeException(
1104
                'Failed to parse safe base64 encoded identity',
1105
                RealMeException::FAILED_PARSING_IDENTITY
1106
            );
1107
        }
1108
1109
        if ($source === FederatedIdentity::SOURCE_XML) {
1110
            $identityDoc = new DOMDocument();
1111
            if ($identityDoc->loadXML($identity)) {
1112
                $federatedIdentity = FederatedIdentity::createFromXML($identityDoc, $nameId);
1113
            }
1114
        }
1115
1116
        if ($source === FederatedIdentity::SOURCE_JSON) {
1117
            $federatedIdentity = FederatedIdentity::createFromJSON($identity, $nameId);
1118
        }
1119
1120
        return $federatedIdentity;
1121
    }
1122
1123
    /**
1124
     * Finds a human-readable error message based on the error code provided in the RealMe SAML response
1125
     *
1126
     * @return string|null The human-readable error message, or null if one can't be found
1127
     */
1128
    private function findErrorMessageForCode($errorCode)
1129
    {
1130
        $message = null;
1131
        $messageOverrides = $this->config()->realme_error_message_overrides;
1132
1133
        switch ($errorCode) {
1134
            case self::ERR_AUTHN_FAILED:
1135
                $message = _t(self::class . '.ERROR_AUTHNFAILED', 'You have chosen to leave RealMe.');
1136
                break;
1137
1138
            case self::ERR_TIMEOUT:
1139
                $message = _t(self::class . '.ERROR_TIMEOUT', 'Your RealMe session has timed out – please try again.');
1140
                break;
1141
1142
            case self::ERR_INTERNAL_ERROR:
1143
                $message = _t(
1144
                    self::class . '.ERROR_INTERNAL',
1145
                    'RealMe was unable to process your request due to a RealMe internal error. Please try again. ' .
1146
                        'If the problem persists, please contact the RealMe Help Desk. From New Zealand dial ' .
1147
                        '0800 664 774 (toll free), from overseas dial +64 4 462 0674 (overseas call charges apply).'
1148
                );
1149
                break;
1150
1151
            case self::ERR_NO_AVAILABLE_IDP:
1152
                $message = _t(
1153
                    self::class . '.ERROR_NOAVAILABLEIDP',
1154
                    'RealMe reported that the TXT service or the token service is not available. You may try again ' .
1155
                        'later. If the problem persists, please contact the RealMe Help Desk. From New Zealand dial ' .
1156
                        '0800 664 774 (toll free), from overseas dial +64 4 462 0674 (overseas call charges apply).'
1157
                );
1158
                break;
1159
1160
            case self::ERR_REQUEST_UNSUPPORTED:
1161
                $message = _t(
1162
                    self::class . '.ERROR_REQUESTUNSUPPORTED',
1163
                    'RealMe reported a serious application error with the message \'Request Unsupported\'. Please try' .
1164
                        ' again later. If the problem persists, please contact the RealMe Help Desk. From New Zealand' .
1165
                        ': 0800 664 774 (toll free), from overseas dial +64 4 462 0674 (overseas call charges apply).'
1166
                );
1167
                break;
1168
1169
            case self::ERR_NO_PASSIVE:
1170
                $message = _t(
1171
                    self::class . '.ERROR_NOPASSIVE',
1172
                    'RealMe reported a serious application error with the message \'No Passive\'. Please try again ' .
1173
                        'later. If the problem persists, please contact the RealMe Help Desk. From New Zealand: 0800 ' .
1174
                        '664 774 (toll free), from overseas dial +64 4 462 0674 (overseas call charges apply).'
1175
                );
1176
                break;
1177
1178
            case self::ERR_REQUEST_DENIED:
1179
                $message = _t(
1180
                    self::class . '.ERROR_REQUESTDENIED',
1181
                    'RealMe reported a serious application error with the message \'Request Denied\'. Please try ' .
1182
                        'again later. If the problem persists, please contact the RealMe Help Desk. From New Zealand:' .
1183
                        ' 0800 664 774 (toll free), from overseas dial +64 4 462 0674 (overseas call charges apply).'
1184
                );
1185
                break;
1186
1187
            case self::ERR_UNSUPPORTED_BINDING:
1188
                $message = _t(
1189
                    self::class . '.ERROR_UNSUPPORTEDBINDING',
1190
                    'RealMe reported a serious application error with the message \'Unsupported Binding\'. Please ' .
1191
                        'try again later. If the problem persists, please contact the RealMe Help Desk. From New ' .
1192
                        'Zealand: 0800 664 774 (toll free), from overseas dial +64 4 462 0674 (overseas call charges ' .
1193
                        'apply).'
1194
                );
1195
                break;
1196
1197
            case self::ERR_UNKNOWN_PRINCIPAL:
1198
                $message = _t(
1199
                    self::class . '.ERROR_UNKNOWNPRINCIPAL',
1200
                    'You are unable to use RealMe to verify your identity if you do not have a RealMe account. ' .
1201
                        'Visit the RealMe home page for more information and to create an account.'
1202
                );
1203
                break;
1204
1205
            case self::ERR_NO_AUTHN_CONTEXT:
1206
                $message = _t(
1207
                    self::class . '.ERROR_NOAUTHNCONTEXT',
1208
                    'RealMe reported a serious application error with the message \'No AuthN Context\'. Please try ' .
1209
                        'again later. If the problem persists, please contact the RealMe Help Desk. From New Zealand:' .
1210
                        ' 0800 664 774 (toll free), from overseas dial +64 4 462 0674 (overseas call charges apply).'
1211
                );
1212
                break;
1213
1214
            default:
1215
                $message = _t(
1216
                    self::class . '.ERROR_GENERAL',
1217
                    'RealMe reported a serious application error. Please try again later. If the problem persists, ' .
1218
                        'please contact the RealMe Help Desk. From New Zealand: 0800 664 774 (toll free), from ' .
1219
                        'overseas dial +64 4 462 0674 (overseas call charges apply).'
1220
                );
1221
                break;
1222
        }
1223
1224
        // Allow message overrides if they exist
1225
        if (array_key_exists($errorCode, $messageOverrides) && !is_null($messageOverrides[$errorCode])) {
1226
            $message = $messageOverrides[$errorCode];
1227
        }
1228
1229
        return $message;
1230
    }
1231
}
1232