Passed
Pull Request — master (#40)
by Indy
01:55
created

RealMeService::parseIdentity()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 3
nop 1
dl 0
loc 22
rs 9.6111
c 0
b 0
f 0
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\FederatedAddress;
23
use SilverStripe\RealMe\Model\FederatedIdentity;
24
use SilverStripe\RealMe\Model\User;
25
use SilverStripe\Security\Member;
26
use SilverStripe\Security\Security;
27
use SilverStripe\View\TemplateGlobalProvider;
28
29
class RealMeService implements TemplateGlobalProvider
30
{
31
    use Configurable, Injectable;
32
33
    /**
34
     * Current RealMe supported environments.
35
     */
36
    const ENV_MTS = 'mts';
37
    const ENV_ITE = 'ite';
38
    const ENV_PROD = 'prod';
39
40
    /**
41
     * SAML binding types
42
     */
43
    const TYPE_LOGIN = 'login';
44
    const TYPE_ASSERT = 'assert';
45
46
    /**
47
     * the valid AuthN context values for each supported RealMe environment.
48
     */
49
    const AUTHN_LOW_STRENGTH = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:LowStrength';
50
    const AUTHN_MOD_STRENTH = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength';
51
    const AUTHN_MOD_MOBILE_SMS =
52
        'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Mobile:SMS';
53
    const AUTHN_MOD_TOKEN_SID =
54
        'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Token:SID';
55
56
    /**
57
     * Realme SAML2 error status constants
58
     */
59
    const ERR_TIMEOUT                = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:status:Timeout';
60
    const ERR_INTERNAL_ERROR         = 'urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:status:InternalError';
61
62
    /**
63
     * SAML2 Error constants used for business logic and switching error messages
64
     */
65
    const ERR_AUTHN_FAILED           = 'urn:oasis:names:tc:SAML:2.0:status:AuthnFailed';
66
    const ERR_UNKNOWN_PRINCIPAL      = 'urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal';
67
    const ERR_NO_AVAILABLE_IDP       = 'urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP';
68
    const ERR_NO_PASSIVE             = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive';
69
    const ERR_NO_AUTHN_CONTEXT       = 'urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext';
70
    const ERR_REQUEST_UNSUPPORTED    = 'urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported';
71
    const ERR_REQUEST_DENIED         = 'urn:oasis:names:tc:SAML:2.0:status:RequestDenied';
72
    const ERR_UNSUPPORTED_BINDING    = 'urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding';
73
74
    const ATTRIBUTE_TYPE_IVS         = 'urn:nzl:govt:ict:stds:authn:safeb64:attribute:igovt:IVS:Assertion:Identity';
75
    const ATTRIBUTE_TYPE_FIT         = 'urn:nzl:govt:ict:stds:authn:attribute:igovt:IVS:FIT';
76
    const ATTRIBUTE_TYPE_AVS         = 'urn:nzl:govt:ict:stds:authn:safeb64:attribute:NZPost:AVS:Assertion:Address';
77
78
    /**
79
     * @var bool true to sync RealMe data and create/update local {@link Member} objects upon successful authentication
80
     * @config
81
     */
82
    private static $sync_with_local_member_database = false;
83
84
    /**
85
     * @var User|null User data returned by RealMe. Provided by {@link self::ensureLogin()}.
86
     *
87
     * Data within this ArrayData is as follows:
88
     * - NameID:       ArrayData   Includes the UserFlt and associated formatting information
89
     * - UserFlt:      string      RealMe pseudonymous username / identity
90
     * - Attributes:   ArrayData   User attributes returned by RealMe
91
     * - Expire:       SS_Datetime The expiry date & time of this authentication session
92
     * - SessionIndex: string      Unique identifier used to identify a user with both IdP and SP for given user.
93
     */
94
    private static $user_data = null;
95
96
    /**
97
     * @config
98
     * @var string The RealMe environment to connect to and authenticate against. This should be set by Config, and
99
     * generally be different per SilverStripe environment (e.g. developer environments would generally use 'mts',
100
     * UAT/staging sites might use 'ite', and production sites would use 'prod'.
101
     *
102
     * Valid options:
103
     * - mts
104
     * - ite
105
     * - prod
106
     */
107
    private static $realme_env = 'mts';
108
109
    /**
110
     * @var array The RealMe environments that can be configured for use with this module.
111
     */
112
    private static $allowed_realme_environments = array(self::ENV_MTS, self::ENV_ITE, self::ENV_PROD);
113
114
    /**
115
     * @config
116
     * @var string The RealMe integration type to use when connecting to RealMe. After successful authentication:
117
     * - 'login' provides a unique FLT (Federated Login Token) back
118
     * - 'assert' provides a unique FIT (Federated Identity Token) and a {@link RealMeFederatedIdentity} object back
119
     */
120
    private static $integration_type = 'login';
121
122
    private static $allowed_realme_integration_types = array(self::TYPE_LOGIN, self::TYPE_ASSERT);
123
124
    /**
125
     * @config
126
     * @var array Stores the entity ID value for each supported RealMe environment. This needs to be setup prior to
127
     * running the `RealMeSetupTask` build task. For more information, see the module documentation. An entity ID takes
128
     * the form of a URL, e.g. https://www.agency.govt.nz/privacy-realm-name/application-name
129
     */
130
    private static $sp_entity_ids = array(
131
        self::ENV_MTS => null,
132
        self::ENV_ITE => null,
133
        self::ENV_PROD => null
134
    );
135
136
    /**
137
     * @config
138
     * @var array Stores the default identity provider (IdP) entity IDs. These can be customised if you're using an
139
     * intermediary IdP instead of connecting to RealMe directly.
140
     */
141
    private static $idp_entity_ids = array(
142
        self::ENV_MTS => array(
143
            self::TYPE_LOGIN  => 'https://mts.realme.govt.nz/saml2',
144
            self::TYPE_ASSERT => 'https://mts.realme.govt.nz/realmemts/realmeidp',
145
        ),
146
        self::ENV_ITE => array(
147
            self::TYPE_LOGIN  => 'https://www.ite.logon.realme.govt.nz/saml2',
148
            self::TYPE_ASSERT => 'https://www.ite.account.realme.govt.nz/saml2/assertion',
149
        ),
150
        self::ENV_PROD => array(
151
            self::TYPE_LOGIN  => 'https://www.logon.realme.govt.nz/saml2',
152
            self::TYPE_ASSERT => 'https://www.account.realme.govt.nz/saml2/assertion',
153
        )
154
    );
155
156
    private static $idp_sso_service_urls = array(
157
        self::ENV_MTS => array(
158
            self::TYPE_LOGIN  => 'https://mts.realme.govt.nz/logon-mts/mtsEntryPoint',
159
            self::TYPE_ASSERT => 'https://mts.realme.govt.nz/realme-mts/validate/realme-mts-idp.xhtml'
160
        ),
161
        self::ENV_ITE => array(
162
            self::TYPE_LOGIN  => 'https://www.ite.logon.realme.govt.nz/sso/logon/metaAlias/logon/logonidp',
163
            self::TYPE_ASSERT => 'https://www.ite.assert.realme.govt.nz/sso/SSORedirect/metaAlias/assertion/realmeidp'
164
        ),
165
        self::ENV_PROD => array(
166
            self::TYPE_LOGIN  => 'https://www.logon.realme.govt.nz/sso/logon/metaAlias/logon/logonidp',
167
            self::TYPE_ASSERT => 'https://www.assert.realme.govt.nz/sso/SSORedirect/metaAlias/assertion/realmeidp'
168
        )
169
    );
170
171
    /**
172
     * @var array A list of certificate filenames for different RealMe environments and integration types. These files
173
     * must be located in the directory specified by the REALME_CERT_DIR environment variable. These filenames are the
174
     * same as the files that can be found in the RealMe Shared Workspace, within the 'Integration Bundle' ZIP files for
175
     * the different environments (MTS, ITE and Production), so you just need to extract the specific certificate file
176
     * that you need and make sure it's in place on the server in the REALME_CERT_DIR.
177
     */
178
    private static $idp_x509_cert_filenames = array(
179
        self::ENV_MTS => array(
180
            self::TYPE_LOGIN  => 'mts_login_saml_idp.cer',
181
            self::TYPE_ASSERT => 'mts_assert_saml_idp.cer'
182
        ),
183
        self::ENV_ITE => array(
184
            self::TYPE_LOGIN  => 'ite.signing.logon.realme.govt.nz.cer',
185
            self::TYPE_ASSERT => 'ite.signing.account.realme.govt.nz.cer'
186
        ),
187
        self::ENV_PROD => array(
188
            self::TYPE_LOGIN  => 'signing.logon.realme.govt.nz.cer',
189
            self::TYPE_ASSERT => 'signing.account.realme.govt.nz.cer'
190
        )
191
    );
192
193
    /**
194
     * @config
195
     * @var array Stores the AuthN context values for each supported RealMe environment. This needs to be setup prior to
196
     * running the `RealMeSetupTask` build task. For more information, see the module documentation. An AuthN context
197
     * can be one of the following:
198
     *
199
     * Username and password only:
200
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:LowStrength
201
     *
202
     * Username, password, and any moderate strength second level of authenticator (RSA token, Google Auth, SMS)
203
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength
204
     *
205
     * The following two are less often used, and shouldn't be used unless there's a specific need.
206
     *
207
     * Username, password, and only SMS 2FA token
208
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Mobile:SMS
209
     *
210
     * Username, password, and only RSA 2FA token
211
     * - urn:nzl:govt:ict:stds:authn:deployment:GLS:SAML:2.0:ac:classes:ModStrength::OTP:Token:SID
212
     */
213
    private static $authn_contexts = array(
214
        self::ENV_MTS => null,
215
        self::ENV_ITE => null,
216
        self::ENV_PROD => null
217
    );
218
219
    /**
220
     * @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...
221
     * @var $allowed_authn_context_list array
222
     *
223
     * A list of the valid authn context values supported for realme.
224
     */
225
    private static $allowed_authn_context_list = array(
226
        self::AUTHN_LOW_STRENGTH,
227
        self::AUTHN_MOD_STRENTH,
228
        self::AUTHN_MOD_MOBILE_SMS,
229
        self::AUTHN_MOD_TOKEN_SID
230
    );
231
232
    /**
233
     * @config
234
     * @var array Domain names for metadata files. Used in @link RealMeSetupTask when outputting metadata XML
235
     */
236
    private static $metadata_assertion_service_domains = array(
237
        self::ENV_MTS => null,
238
        self::ENV_ITE => null,
239
        self::ENV_PROD => null
240
    );
241
242
    /**
243
     * @config
244
     * @var array A list of error messages to display if RealMe returns error statuses, instead of the default
245
     * translations (found in realme/lang/en.yml for example).
246
     */
247
    private static $realme_error_message_overrides = array(
248
        self::ERR_AUTHN_FAILED => null,
249
        self::ERR_TIMEOUT => null,
250
        self::ERR_INTERNAL_ERROR => null,
251
        self::ERR_NO_AVAILABLE_IDP => null,
252
        self::ERR_REQUEST_UNSUPPORTED => null,
253
        self::ERR_NO_PASSIVE => null,
254
        self::ERR_REQUEST_DENIED => null,
255
        self::ERR_UNSUPPORTED_BINDING => null,
256
        self::ERR_UNKNOWN_PRINCIPAL => null,
257
        self::ERR_NO_AUTHN_CONTEXT => null
258
    );
259
260
    /**
261
     * @config
262
     * @var string|null The organisation name to be used in metadata XML that is submitted to RealMe
263
     */
264
    private static $metadata_organisation_name = null;
265
266
    /**
267
     * @config
268
     * @var string|null The organisation display name to be used in metadata XML that is submitted to RealMe
269
     */
270
    private static $metadata_organisation_display_name = null;
271
272
    /**
273
     * @config
274
     * @var string|null The organisation URL to be used in metadata XML that is submitted to RealMe
275
     */
276
    private static $metadata_organisation_url = null;
277
278
    /**
279
     * @config
280
     * @var string|null The support contact's company name to be used in metadata XML that is submitted to RealMe
281
     */
282
    private static $metadata_contact_support_company = null;
283
284
    /**
285
     * @config
286
     * @var string|null The support contact's first name(s) to be used in metadata XML that is submitted to RealMe
287
     */
288
    private static $metadata_contact_support_firstnames = null;
289
290
    /**
291
     * @config
292
     * @var string|null The support contact's surname to be used in metadata XML that is submitted to RealMe
293
     */
294
    private static $metadata_contact_support_surname = null;
295
296
    /**
297
     * @var OneLogin_Saml2_Auth|null Set by {@link getAuth()}, which creates an instance of OneLogin_Saml2_Auth to check
298
     * authentication against
299
     */
300
    private $auth = null;
301
302
    /**
303
     * @var string|null The last error message during login enforcement
304
     */
305
    private $lastError = null;
306
307
    /**
308
     * @return array
309
     */
310
    public static function get_template_global_variables()
311
    {
312
        return array(
313
            'RealMeUser' => array(
314
                'method' => 'current_realme_user'
315
            )
316
        );
317
    }
318
319
    /**
320
     * @return HTTPRequest|null
321
     */
322
    protected static function getRequest()
323
    {
324
        if (!Injector::inst()->has(HTTPRequest::class)) {
325
            return null;
326
        };
327
328
        return Injector::inst()->get(HTTPRequest::class);
329
    }
330
331
    /**
332
     * Return the user data which was saved to session from the first RealMe
333
     * auth.
334
     * Note: Does not check authenticity or expiry of this data
335
     *
336
     * @param HTTPRequest $request
337
     * @return User
338
     */
339
    public static function user_data()
340
    {
341
        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...
342
            return static::$user_data;
343
        }
344
345
        $request = self::getRequest();
346
347
        if (!$request) {
348
            return null;
349
        }
350
351
        $sessionData = $request->getSession()->get('RealMe.SessionData');
352
353
        // Exit point
354
        if (is_null($sessionData)) {
355
            return null;
356
        }
357
358
        // Unserialise stored data
359
        $user = unserialize($sessionData);
360
361
        if ($user == false || !$user instanceof User) {
362
            return null;
363
        }
364
365
        static::$user_data = $user;
366
        return static::$user_data;
367
    }
368
369
    public function getUserData()
370
    {
371
        return static::user_data();
372
    }
373
374
    /**
375
     * Calls available user data and checks for validity
376
     *
377
     * @return User
378
     */
379
    public static function current_realme_user()
380
    {
381
        $user = self::user_data();
382
        if ($user && !$user->isValid()) {
383
            return null;
384
        }
385
386
        return $user;
387
    }
388
389
    /**
390
     * A helpful static method that follows SilverStripe naming for Member::currentUser();
391
     *
392
     * @return User
393
     */
394
    public static function currentRealMeUser()
395
    {
396
        return self::current_realme_user();
397
    }
398
399
    /**
400
     * Enforce login via RealMe. This can be used in controllers to force users to be authenticated via RealMe (not
401
     * necessarily logged in as a {@link Member}), in the form of:
402
     * <code>
403
     * Session::set('RealMeBackURL', '/path/to/the/controller/method');
404
     * if($service->enforceLogin()) {
405
     *     // User has a valid RealMe account, $service->getAuthData() will return you their details
406
     * } else {
407
     *     // Something went wrong processing their details, show an error
408
     * }
409
     * </code>
410
     *
411
     * In cases where people are *not* authenticated with RealMe, this method will redirect them directly to RealMe.
412
     *
413
     * However, generally you want this to be an explicit process, so you should look at instead using the standard
414
     * {@link RealMeAuthenticator}.
415
     *
416
     * A return value of bool false indicates that there was a failure during the authentication process (perhaps a
417
     * communication issue, or a failure to decode the response correctly. You should handle this like you would any
418
     * other unexpected authentication error. You can use {@link getLastError()} to see if a human-readable error
419
     * message exists for display to the user.
420
     *
421
     * @param HTTPRequest $request
422
     * @param string $backUrl
423
     * @return bool|null true if the user is correctly authenticated, false if there was an error with login
424
     * @throws OneLogin_Saml2_Error
425
     */
426
    public function enforceLogin(HTTPRequest $request, $backUrl = null)
427
    {
428
        // First, check to see if we have an existing authenticated session
429
        if ($this->isAuthenticated()) {
430
            return true;
431
        }
432
433
        $session = $request->getSession();
434
435
        if ($backUrl) {
436
            $session->set('RealMeBackURL', $this->validSiteURL($backUrl));
437
        }
438
439
        // If not, attempt to retrieve authentication data from OneLogin (in case this is called during SAML assertion)
440
        try {
441
            if (!$session->get("RealMeErrorBackURL")) {
442
                $session->set("RealMeErrorBackURL", Controller::curr()->Link("Login"));
443
            }
444
445
            $auth = $this->getAuth();
446
            $auth->processResponse();
447
448
            // if there were any errors from the SAML request, process and translate them.
449
            $errors = $auth->getErrors();
450
            if (is_array($errors) && !empty($errors)) {
451
                $this->processSamlErrors($errors);
452
                return false;
453
            }
454
455
            $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...
456
457
            // If no data is found, then force login
458
            if (is_null($authData)) {
0 ignored issues
show
introduced by
The condition is_null($authData) is always true.
Loading history...
459
                throw new RealMeException('No SAML data, enforcing login', RealMeException::NOT_AUTHENTICATED);
460
            }
461
462
            // call a success method as we've successfully logged in (if it exists)
463
            Member::singleton()->extend('onRealMeLoginSuccess', $authData);
464
        } catch (BaseException $e) {
465
            Member::singleton()->extend("onRealMeLoginFailure", $e);
466
467
            // No auth data or failed to decrypt, enforce login again
468
            $auth->login(Director::absoluteBaseURL());
469
            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...
470
        }
471
472
        return $auth->isAuthenticated();
473
    }
474
475
    /**
476
     * If there was an error returned from the saml response, process the errors
477
     *
478
     * @param $errors
479
     */
480
    private function processSamlErrors(array $errors)
481
    {
482
        $translatedMessage = null;
483
484
        // The error message returned by onelogin/php-saml is the top-level error, but we want the actual error
485
        $request = Controller::curr()->getRequest();
486
        if ($request->isPOST() && $request->postVar("SAMLResponse")) {
487
            $response = new OneLogin_Saml2_Response($this->getAuth()->getSettings(), $request->postVar("SAMLResponse"));
488
            $internalError = OneLogin_Saml2_Utils::query(
489
                $response->document,
490
                "/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode/@Value"
491
            );
492
493
            if ($internalError instanceof DOMNodeList && $internalError->length > 0) {
494
                $internalErrorCode = $internalError->item(0)->textContent;
495
                $translatedMessage = $this->findErrorMessageForCode($internalErrorCode);
496
            }
497
        }
498
499
        // If we found a message to display, then let's redirect to the form and display it
500
        if ($translatedMessage) {
501
            $this->lastError = $translatedMessage;
502
        }
503
504
        Injector::inst()->get(LoggerInterface::class)->info(sprintf(
505
            'onelogin/php-saml error messages: %s (%s)',
506
            join(', ', $errors),
507
            $this->getAuth()->getLastErrorReason()
508
        ));
509
    }
510
511
    /**
512
     * Checks data stored in Session to see if the user is authenticated.
513
     * @return bool true if the user is authenticated via RealMe and we can trust ->getUserData()
514
     */
515
    public function isAuthenticated()
516
    {
517
        $user = $this->getUserData();
518
        return $user instanceof User && $user->isAuthenticated();
519
    }
520
521
    /**
522
     * Returns a {@link RealMeUser} object if one can be built from the RealMe session data.
523
     *
524
     * @throws OneLogin_Saml2_Error Passes on the SAML error if it's not indicating a lack of SAML response data
525
     * @throws RealMeException If identity information exists but couldn't be decoded, or doesn't exist
526
     * @return User|null
527
     */
528
    public function getAuthData()
529
    {
530
        // returns null if the current auth is invalid or timed out.
531
        try {
532
            // Process response and capture details
533
            $auth = $this->getAuth();
534
535
            if (!$auth->isAuthenticated()) {
536
                throw new RealMeException(
537
                    'OneLogin SAML library did not successfully authenticate, but did not return a specific error',
538
                    RealMeException::NOT_AUTHENTICATED
539
                );
540
            }
541
542
            $spNameId = $auth->getNameId();
543
            if (!is_string($spNameId)) {
0 ignored issues
show
introduced by
The condition is_string($spNameId) is always true.
Loading history...
544
                throw new RealMeException('Invalid/Missing NameID in SAML response', RealMeException::MISSING_NAMEID);
545
            }
546
547
            $sessionIndex = $auth->getSessionIndex();
548
            if (!is_string($sessionIndex)) {
0 ignored issues
show
introduced by
The condition is_string($sessionIndex) is always true.
Loading history...
549
                throw new RealMeException(
550
                    'Invalid/Missing SessionIndex value in SAML response',
551
                    RealMeException::MISSING_SESSION_INDEX
552
                );
553
            }
554
555
            $attributes = $auth->getAttributes();
556
            if (!is_array($attributes)) {
0 ignored issues
show
introduced by
The condition is_array($attributes) is always true.
Loading history...
557
                throw new RealMeException(
558
                    'Invalid/Missing attributes array in SAML response',
559
                    RealMeException::MISSING_ATTRIBUTES
560
                );
561
            }
562
563
            $federatedIdentity = $this->retrieveFederatedIdentity($auth);
564
565
            // We will have either a FLT or FIT, depending on integration type
566
            if ($this->config()->integration_type == self::TYPE_ASSERT) {
567
                $userTag = $this->retrieveFederatedIdentityTag($auth);
568
            } else {
569
                $userTag = $this->retrieveFederatedLogonTag($auth);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $userTag is correct as $this->retrieveFederatedLogonTag($auth) targeting SilverStripe\RealMe\Real...ieveFederatedLogonTag() 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...
570
            }
571
572
            return User::create([
573
                'SPNameID' => $spNameId,
574
                'UserFederatedTag' => $userTag,
575
                'SessionIndex' => $sessionIndex,
576
                'Attributes' => $attributes,
577
                'FederatedIdentity' => $federatedIdentity,
578
            ]);
579
        } catch (OneLogin_Saml2_Error $e) {
580
            // If the Exception code indicates there wasn't a response, we ignore it as it simply means the visitor
581
            // isn't authenticated yet. Otherwise, we re-throw the Exception
582
            if ($e->getCode() === OneLogin_Saml2_Error::SAML_RESPONSE_NOT_FOUND) {
583
                return null;
584
            } else {
585
                throw $e;
586
            }
587
        }
588
    }
589
590
    /**
591
     * Clear the RealMe credentials from Session, called during Security->logout() overrides
592
     *
593
     * @param HTTPRequest $request
594
     * @return void
595
     */
596
    public function clearLogin(HTTPRequest $request)
597
    {
598
        $this->config()->__set('user_data', null);
599
        $session = $request->getSession();
600
601
        $session->set("RealMeBackURL", null);
602
        $session->set("RealMeErrorBackURL", null);
603
        $session->set("RealMe.SessionData", null);
604
        $session->set("RealMe.OriginalResponse", null);
605
        $session->set("RealMe.LastErrorMessage", null);
606
    }
607
608
    public function getLastError()
609
    {
610
        return $this->lastError;
611
    }
612
613
    /**
614
     * @return string A BackURL as specified originally when accessing /Security/login, for use after authentication
615
     */
616
    public function getBackURL(HTTPRequest $request)
617
    {
618
        $url = null;
619
        $session = $request->getSession();
620
621
        if ($session->get('RealMeBackURL')) {
622
            $url = $session->get('RealMeBackURL');
623
            $session->clear('RealMeBackURL'); // Ensure we don't redirect back to the same error twice
624
        }
625
626
        return $this->validSiteURL($url);
627
    }
628
629
    public function getErrorBackURL(HTTPRequest $request)
630
    {
631
        $url = null;
632
        $session = $request->getSession();
633
634
        if ($session->get('RealMeErrorBackURL')) {
635
            $url = $session->get('RealMeErrorBackURL');
636
            $session->clear('RealMeErrorBackURL'); // Ensure we don't redirect back to the same error twice
637
        }
638
639
        return $this->validSiteURL($url);
640
    }
641
642
    private function validSiteURL($url = null)
643
    {
644
        if (isset($url) && Director::is_site_url($url)) {
645
            $url = Director::absoluteURL($url);
646
        } else {
647
            // Spoofing attack or no back URL set, redirect to homepage instead of spoofing url
648
            $url = Director::absoluteBaseURL();
649
        }
650
651
        return $url;
652
    }
653
654
    /**
655
     * @param String $subdir A sub-directory where certificates may be stored for
656
     * a specific case
657
     * @return string|null Either the directory where certificates are stored,
658
     * or null if undefined
659
     */
660
    public function getCertDir($subdir = null)
661
    {
662
663
        // Trim prepended seprator to avoid absolute path
664
        $path = ltrim(ltrim($subdir, '/'), '\\');
665
666
        if ($certDir = Environment::getEnv('REALME_CERT_DIR')) {
667
            $path = $certDir . '/' . $path; // Duplicate slashes will be handled by realpath()
668
        }
669
670
        return realpath($path);
671
    }
672
673
    /**
674
     * Returns the appropriate AuthN Context, given the environment passed in. The AuthNContext may be different per
675
     * environment, and should be one of the strings as defined in the static {@link self::$authn_contexts} at the top
676
     * of this class.
677
     *
678
     * @param string $env The environment to return the AuthNContext for. Must be one of the RealMe environment names
679
     * @return string|null Returns the AuthNContext for the given $env, or null if no context exists
680
     */
681
    public function getAuthnContextForEnvironment($env)
682
    {
683
        return $this->getConfigurationVarByEnv('authn_contexts', $env);
684
    }
685
686
    /**
687
     * Returns the full path to the SAML signing certificate file, used by SimpleSAMLphp to sign all messages sent to
688
     * RealMe.
689
     *
690
     * @return string|null Either the full path to the SAML signing certificate file, or null if it doesn't exist
691
     */
692
    public function getSigningCertPath()
693
    {
694
        return $this->getCertPath('SIGNING');
695
    }
696
697
    public function getIdPCertPath()
698
    {
699
        $cfg = $this->config();
700
        $name = $this->getConfigurationVarByEnv('idp_x509_cert_filenames', $cfg->realme_env, $cfg->integration_type);
701
702
        return $this->getCertDir($name);
703
    }
704
705
    public function getSPCertContent($contentType = 'certificate')
706
    {
707
        return $this->getCertificateContents($this->getSigningCertPath(), $contentType);
708
    }
709
710
    public function getIdPCertContent()
711
    {
712
        return $this->getCertificateContents($this->getIdPCertPath());
713
    }
714
715
    /**
716
     * Returns the content of the SAML signing certificate. This is used by getAuth() and by RealMeSetupTask to produce
717
     * metadata XML files.
718
     *
719
     * @param string $certPath The filesystem path to where the certificate is stored on the filesystem
720
     * @param string $contentType Either 'certificate' or 'key', depending on which part of the file to return
721
     * @return string|null The content of the signing certificate
722
     */
723
    public function getCertificateContents($certPath, $contentType = 'certificate')
724
    {
725
        $text = null;
726
727
        if (!is_null($certPath)) {
0 ignored issues
show
introduced by
The condition is_null($certPath) is always false.
Loading history...
728
            $certificateContents = file_get_contents($certPath);
729
730
            // If the file does not contain any header information and the content type is certificate, just return it
731
            if ($contentType == 'certificate' && !preg_match('/-----BEGIN/', $certificateContents)) {
732
                $text = $certificateContents;
733
            } else {
734
                // Otherwise, inspect the file and match based on the full contents
735
                if ($contentType == 'certificate') {
736
                    $pattern = '/-----BEGIN CERTIFICATE-----[\r\n]*([^-]*)[\r\n]*-----END CERTIFICATE-----/';
737
                } elseif ($contentType == 'key') {
738
                    $pattern = '/-----BEGIN [A-Z ]*PRIVATE KEY-----[\r\n]*([^-]*)[\r\n]*'
739
                        . '-----END [A-Z ]*PRIVATE KEY-----/';
740
                } else {
741
                    throw new InvalidArgumentException('Argument contentType must be either "certificate" or "key"');
742
                }
743
744
                // This is a PEM key, and we need to extract just the certificate, stripping out the private key etc.
745
                // So we search for everything between '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----'
746
                preg_match(
747
                    $pattern,
748
                    $certificateContents,
749
                    $matches
750
                );
751
752
                if (isset($matches) && is_array($matches) && isset($matches[1])) {
753
                    $text = trim($matches[1]);
754
                }
755
            }
756
        }
757
758
        return $text;
759
    }
760
761
    /**
762
     * @param string $env The environment to return the entity ID for. Must be one of the RealMe environment names
763
     * @return string|null Either the assertion consumer service location, or null if information doesn't exist
764
     */
765
    public function getAssertionConsumerServiceUrlForEnvironment($env)
766
    {
767
        if (in_array($env, $this->getAllowedRealMeEnvironments()) === false) {
768
            return null;
769
        }
770
771
        $domain = $this->getMetadataAssertionServiceDomainForEnvironment($env);
772
        if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
773
            return null;
774
        }
775
776
        // Returns https://domain.govt.nz/Security/login/RealMe/acs
777
        return Controller::join_links($domain, Security::config()->get('login_url'), 'RealMe/acs');
778
    }
779
780
    /**
781
     * @return string|null The organisation name to be used in metadata XML output, or null if none exists
782
     */
783
    public function getMetadataOrganisationName()
784
    {
785
        $orgName = $this->config()->metadata_organisation_name;
786
        return (strlen($orgName) > 0) ? $orgName : null;
787
    }
788
789
    /**
790
     * @return string|null The organisation display name to be used in metadata XML output, or null if none exists
791
     */
792
    public function getMetadataOrganisationDisplayName()
793
    {
794
        $displayName = $this->config()->metadata_organisation_display_name;
795
        return (strlen($displayName) > 0) ? $displayName : null;
796
    }
797
798
    /**
799
     * @return string|null The organisation website URL to be used in metadata XML output, or null if none exists
800
     */
801
    public function getMetadataOrganisationUrl()
802
    {
803
        $url = $this->config()->metadata_organisation_url;
804
        return (strlen($url) > 0) ? $url: null;
805
    }
806
807
    /**
808
     * @return string[] The support contact details to be used in metadata XML output, with null values if they don't
809
     *                  exist
810
     */
811
    public function getMetadataContactSupport()
812
    {
813
        $company = $this->config()->metadata_contact_support_company;
814
        $firstNames = $this->config()->metadata_contact_support_firstnames;
815
        $surname = $this->config()->metadata_contact_support_surname;
816
817
        return array(
818
            'company' => (strlen($company) > 0) ? $company : null,
819
            'firstNames' => (strlen($firstNames) > 0) ? $firstNames : null,
820
            'surname' => (strlen($surname) > 0) ? $surname : null
821
        );
822
    }
823
824
    /**
825
     * The list of RealMe environments that can be used. By default, we allow mts, ite and production.
826
     * @return array
827
     */
828
    public function getAllowedRealMeEnvironments()
829
    {
830
        return $this->config()->allowed_realme_environments;
831
    }
832
833
    /**
834
     * The list of valid realme AuthNContexts
835
     * @return array
836
     */
837
    public function getAllowedAuthNContextList()
838
    {
839
        return $this->config()->allowed_authn_context_list;
840
    }
841
842
    /**
843
     * Returns the appropriate entity ID for RealMe, given the environment passed in. The entity ID may be different per
844
     * environment, and should be a full URL, including privacy realm and application name. For example, this may be:
845
     * https://www.agency.govt.nz/privacy-realm-name/application-name
846
     *
847
     * @return string|null Returns the entity ID for the current environment, or null if no entity ID exists
848
     */
849
    public function getSPEntityID()
850
    {
851
        return $this->getConfigurationVarByEnv('sp_entity_ids', $this->config()->realme_env);
852
    }
853
854
    private function getIdPEntityID()
855
    {
856
        $cfg = $this->config();
857
        return $this->getConfigurationVarByEnv('idp_entity_ids', $cfg->realme_env, $cfg->integration_type);
858
    }
859
860
    private function getSingleSignOnServiceURL()
861
    {
862
        $cfg = $this->config();
863
        return $this->getConfigurationVarByEnv('idp_sso_service_urls', $cfg->realme_env, $cfg->integration_type);
864
    }
865
866
    private function getRequestedAuthnContext()
867
    {
868
        return $this->getConfigurationVarByEnv('authn_contexts', $this->config()->realme_env);
869
    }
870
871
    /**
872
     * Returns the internal {@link OneLogin_Saml2_Auth} object against which visitors are authenticated.
873
     *
874
     * @return OneLogin_Saml2_Auth
875
     */
876
    public function getAuth(HTTPRequest $request = null)
877
    {
878
        if (isset($this->auth)) {
879
            return $this->auth;
880
        }
881
882
        if (!$request) {
883
            $request = self::getRequest();
884
            if (!$request) {
885
                throw new RealMeException('A request must be provided for session access');
886
            }
887
        }
888
889
        // Ensure onelogin is using the correct host, protocol and port incase a proxy is involved
890
        OneLogin_Saml2_Utils::setSelfHost($request->getHeader('Host'));
891
        OneLogin_Saml2_Utils::setSelfProtocol($request->getScheme());
892
893
        $port = null;
894
        if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) {
895
            $port = $_SERVER['HTTP_X_FORWARDED_PORT'];
896
        } elseif (isset($_SERVER['SERVER_PORT'])) {
897
            $port = $_SERVER['SERVER_PORT'];
898
        }
899
900
        if ($port) {
901
            OneLogin_Saml2_Utils::setSelfPort($port);
902
        }
903
904
        $settings = [
905
            'strict' => true,
906
            'debug' => false,
907
908
            // Service Provider (this installation) configuration
909
            'sp' => [
910
                'entityId' => $this->getSPEntityID(),
911
                'x509cert' => $this->getSPCertContent('certificate'),
912
                'privateKey' => $this->getSPCertContent('key'),
913
914
                // According to RealMe messaging spec, must always be transient for assert; is irrelevant for login
915
                'NameIDFormat' => $this->getNameIdFormat(),
916
917
                'assertionConsumerService' => [
918
                    'url' => $this->getAssertionConsumerServiceUrlForEnvironment($this->config()->realme_env),
919
                    'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' // Always POST, not artifact binding
920
                ]
921
            ],
922
923
            // RealMe Identity Provider configuration
924
            'idp' => [
925
                'entityId' => $this->getIdPEntityID(),
926
                'x509cert' => $this->getIdPCertContent(),
927
928
                'singleSignOnService' => [
929
                    'url' => $this->getSingleSignOnServiceURL(),
930
                    'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
931
                ]
932
            ],
933
934
            'security' => [
935
                'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
936
                'authnRequestsSigned' => true,
937
                'wantAssertionsEncrypted' => true,
938
                'wantAssertionsSigned' => true,
939
940
                'requestedAuthnContext' => [
941
                    $this->getRequestedAuthnContext()
942
                ]
943
            ]
944
        ];
945
946
        $this->auth = new OneLogin_Saml2_Auth($settings);
947
        return $this->auth;
948
    }
949
950
    /**
951
     * @return string the required NameIDFormat to be included in metadata XML, based on the requested integration type
952
     */
953
    public function getNameIdFormat()
954
    {
955
        switch ($this->config()->integration_type) {
956
            case self::TYPE_ASSERT:
957
                return 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient';
958
                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...
959
960
            case self::TYPE_LOGIN:
961
            default:
962
                return 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent';
963
                break;
964
        }
965
    }
966
967
    /**
968
     * @param string $cfgName The static configuration value to get. This should be an array
969
     * @param string $env The environment to return the value for. Must be one of the RealMe environment names
970
     * @param string $integrationType The integration type (login or assert), if necessary, to determine return var
971
     * @throws InvalidArgumentException If the cfgVar doesn't exist, or is malformed
972
     * @return string|null Returns the value as defined in $cfgName for the given environment, or null if none exist
973
     */
974
    private function getConfigurationVarByEnv($cfgName, $env, $integrationType = null)
975
    {
976
        $value = null;
977
978
        if (in_array($env, $this->getAllowedRealMeEnvironments())) {
979
            $values = $this->config()->$cfgName;
980
981
            if (is_array($values) && isset($values[$env])) {
982
                $value = $values[$env];
983
            }
984
        }
985
986
        // If $integrationType is specified, then $value should be an array, with the array key being the integration
987
        // type and array value being the returned variable
988
        if (!is_null($integrationType) && is_array($value) && isset($value[$integrationType])) {
989
            $value = $value[$integrationType];
990
        } elseif (!is_null($integrationType)) {
991
            // Otherwise, we are expecting an integration type, but the value is not specified that way, error out
992
            throw new InvalidArgumentException(
993
                sprintf(
994
                    'Config value %s[%s][%s] not well formed (cfg var not an array)',
995
                    $cfgName,
996
                    $env,
997
                    $integrationType
998
                )
999
            );
1000
        }
1001
1002
        if (is_null($value)) {
1003
            throw new InvalidArgumentException(sprintf('Config value %s[%s] not set', $cfgName, $env));
1004
        }
1005
1006
        return $value;
1007
    }
1008
1009
    /**
1010
     * @param string $certName The certificate name, either 'SIGNING' or 'MUTUAL'
1011
     * @return string|null Either the full path to the certificate file, or null if it doesn't exist
1012
     * @see self::getSigningCertPath()
1013
     */
1014
    private function getCertPath($certName)
1015
    {
1016
        $certPath = null;
1017
1018
        if (in_array($certName, array('SIGNING', 'MUTUAL'))) {
1019
            $constName = sprintf('REALME_%s_CERT_FILENAME', strtoupper($certName));
1020
            if ($filename = Environment::getEnv($constName)) {
1021
                $certPath = $this->getCertDir($filename);
1022
            }
1023
        }
1024
1025
        // Ensure the file exists, if it doesn't then set it to null
1026
        if (!is_null($certPath) && !file_exists($certPath)) {
1027
            $certPath = null;
1028
        }
1029
1030
        return $certPath;
1031
    }
1032
1033
    /**
1034
     * @param string $env The environment to return the domain name for. Must be one of the RealMe environment names
1035
     * @return string|null Either the FQDN (e.g. https://www.realme-demo.govt.nz/) or null if none is specified
1036
     */
1037
    private function getMetadataAssertionServiceDomainForEnvironment($env)
1038
    {
1039
        return $this->getConfigurationVarByEnv('metadata_assertion_service_domains', $env);
1040
    }
1041
1042
    /**
1043
     * @param OneLogin_Saml2_Auth $auth
1044
     * @return string|null null if there's no FLT, or a string if there is one
1045
     */
1046
    private function retrieveFederatedLogonTag(OneLogin_Saml2_Auth $auth)
0 ignored issues
show
Unused Code introduced by
The parameter $auth is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1046
    private function retrieveFederatedLogonTag(/** @scrutinizer ignore-unused */ OneLogin_Saml2_Auth $auth)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1047
    {
1048
        return null; // @todo
1049
    }
1050
1051
    /**
1052
     * @param OneLogin_Saml2_Auth $auth
1053
     * @return string|null null if there's not FIT, or a string if there is one
1054
     */
1055
    private function retrieveFederatedIdentityTag(OneLogin_Saml2_Auth $auth)
1056
    {
1057
        $fit = null;
1058
        $attributes = $auth->getAttributes();
1059
1060
        if (isset($attributes[self::ATTRIBUTE_TYPE_FIT])) {
1061
            $fit = $attributes[self::ATTRIBUTE_TYPE_FIT][0];
1062
        }
1063
1064
        return $fit;
1065
    }
1066
1067
    /**
1068
     * @param OneLogin_Saml2_Auth $auth
1069
     * @return FederatedIdentity|null
1070
     * @throws RealMeException
1071
     */
1072
    private function retrieveFederatedIdentity(OneLogin_Saml2_Auth $auth)
1073
    {
1074
        $federatedIdentity = null;
1075
        $attributes = $auth->getAttributes();
1076
        $nameId = $auth->getNameId();
1077
1078
        // If identity information exists, retrieve the FIT (Federated Identity Tag) and identity data
1079
        if (isset($attributes[self::ATTRIBUTE_TYPE_IVS])) {
1080
            $identity = $this->parseIdentity($attributes[self::ATTRIBUTE_TYPE_IVS]);
1081
1082
            $identityDoc = new DOMDocument();
1083
            if ($identityDoc->loadXML($identity)) {
1084
                $federatedIdentity = new FederatedIdentity($identityDoc, $nameId);
1085
            }
1086
1087
            if ($identityDoc && isset($attributes[self::ATTRIBUTE_TYPE_AVS])) {
1088
                $address = $this->parseIdentity($attributes[self::ATTRIBUTE_TYPE_AVS]);
1089
                $addressDoc = new DOMDocument();
1090
                if ($addressDoc->loadXML($address)) {
1091
                    $federatedIdentity->Address = new FederatedAddress($addressDoc);
1092
                }
1093
            }
1094
        }
1095
1096
        return $federatedIdentity;
1097
    }
1098
1099
    /**
1100
     * Identity information is encoded using 'Base 64 Encoding with URL and Filename Safe Alphabet'
1101
     * For more info, review RFC3548, section 4 (https://tools.ietf.org/html/rfc3548#page-6)
1102
     * Note: This is different to PHP's standard base64_decode() function, therefore we need to swap chars
1103
     * to match PHP's expectations:
1104
     * char 62 (-) becomes +
1105
     * char 63 (_) becomes /
1106
     *
1107
     * @param string $identity
1108
     *
1109
     * @return string
1110
     */
1111
    private function parseIdentity($identity)
1112
    {
1113
        if (!is_array($identity) || !isset($identity[0])) {
0 ignored issues
show
introduced by
The condition is_array($identity) is always false.
Loading history...
1114
            throw new RealMeException(
1115
                'Invalid identity response received from RealMe',
1116
                RealMeException::INVALID_IDENTITY_VALUE
1117
            );
1118
        }
1119
1120
        // Switch from filename-safe alphabet base64 encoding to standard base64 encoding
1121
        $identity = strtr($identity[0], '-_', '+/');
1122
        $identity = base64_decode($identity, true);
1123
1124
        if (is_bool($identity) && !$identity) {
1125
            // Strict base64_decode fails, either the identity didn't exist or was mangled during transmission
1126
            throw new RealMeException(
1127
                'Failed to parse safe base64 encoded identity',
1128
                RealMeException::FAILED_PARSING_IDENTITY
1129
            );
1130
        }
1131
1132
        return $identity;
1133
    }
1134
1135
    /**
1136
     * Finds a human-readable error message based on the error code provided in the RealMe SAML response
1137
     *
1138
     * @return string|null The human-readable error message, or null if one can't be found
1139
     */
1140
    private function findErrorMessageForCode($errorCode)
1141
    {
1142
        $message = null;
1143
        $messageOverrides = $this->config()->realme_error_message_overrides;
1144
1145
        switch ($errorCode) {
1146
            case self::ERR_AUTHN_FAILED:
1147
                $message = _t(self::class . '.ERROR_AUTHNFAILED', 'You have chosen to leave RealMe.');
1148
                break;
1149
1150
            case self::ERR_TIMEOUT:
1151
                $message = _t(self::class . '.ERROR_TIMEOUT', 'Your RealMe session has timed out – please try again.');
1152
                break;
1153
1154
            case self::ERR_INTERNAL_ERROR:
1155
                $message = _t(
1156
                    self::class . '.ERROR_INTERNAL',
1157
                    'RealMe was unable to process your request due to a RealMe internal error. Please try again. ' .
1158
                        'If the problem persists, please contact the RealMe Help Desk. From New Zealand dial ' .
1159
                        '0800 664 774 (toll free), from overseas dial +64 9 357 4468 (overseas call charges apply).'
1160
                );
1161
                break;
1162
1163
            case self::ERR_NO_AVAILABLE_IDP:
1164
                $message = _t(
1165
                    self::class . '.ERROR_NOAVAILABLEIDP',
1166
                    'RealMe reported that the TXT service or the token service is not available. You may try again ' .
1167
                        'later. If the problem persists, please contact the RealMe Help Desk. From New Zealand dial ' .
1168
                        '0800 664 774 (toll free), from overseas dial +64 9 357 4468 (overseas call charges apply).'
1169
                );
1170
                break;
1171
1172
            case self::ERR_REQUEST_UNSUPPORTED:
1173
                $message = _t(
1174
                    self::class . '.ERROR_REQUESTUNSUPPORTED',
1175
                    'RealMe reported a serious application error with the message \'Request Unsupported\'. Please try' .
1176
                        ' again later. If the problem persists, please contact the RealMe Help Desk. From New Zealand' .
1177
                        ': 0800 664 774 (toll free), from overseas dial +64 9 357 4468 (overseas call charges apply).'
1178
                );
1179
                break;
1180
1181
            case self::ERR_NO_PASSIVE:
1182
                $message = _t(
1183
                    self::class . '.ERROR_NOPASSIVE',
1184
                    'RealMe reported a serious application error with the message \'No Passive\'. Please try again ' .
1185
                        'later. If the problem persists, please contact the RealMe Help Desk. From New Zealand: 0800 ' .
1186
                        '664 774 (toll free), from overseas dial +64 9 357 4468 (overseas call charges apply).'
1187
                );
1188
                break;
1189
1190
            case self::ERR_REQUEST_DENIED:
1191
                $message = _t(
1192
                    self::class . '.ERROR_REQUESTDENIED',
1193
                    'RealMe reported a serious application error with the message \'Request Denied\'. Please try ' .
1194
                        'again later. If the problem persists, please contact the RealMe Help Desk. From New Zealand:' .
1195
                        ' 0800 664 774 (toll free), from overseas dial +64 9 357 4468 (overseas call charges apply).'
1196
                );
1197
                break;
1198
1199
            case self::ERR_UNSUPPORTED_BINDING:
1200
                $message = _t(
1201
                    self::class . '.ERROR_UNSUPPORTEDBINDING',
1202
                    'RealMe reported a serious application error with the message \'Unsupported Binding\'. Please ' .
1203
                        'try again later. If the problem persists, please contact the RealMe Help Desk. From New ' .
1204
                        'Zealand: 0800 664 774 (toll free), from overseas dial +64 9 357 4468 (overseas call charges ' .
1205
                        'apply).'
1206
                );
1207
                break;
1208
1209
            case self::ERR_UNKNOWN_PRINCIPAL:
1210
                $message = _t(
1211
                    self::class . '.ERROR_UNKNOWNPRINCIPAL',
1212
                    'You are unable to use RealMe to verify your identity if you do not have a RealMe account. ' .
1213
                        'Visit the RealMe home page for more information and to create an account.'
1214
                );
1215
                break;
1216
1217
            case self::ERR_NO_AUTHN_CONTEXT:
1218
                $message = _t(
1219
                    self::class . '.ERROR_NOAUTHNCONTEXT',
1220
                    'RealMe reported a serious application error with the message \'No AuthN Context\'. Please try ' .
1221
                        'again later. If the problem persists, please contact the RealMe Help Desk. From New Zealand:' .
1222
                        ' 0800 664 774 (toll free), from overseas dial +64 9 357 4468 (overseas call charges apply).'
1223
                );
1224
                break;
1225
1226
            default:
1227
                $message = _t(
1228
                    self::class . '.ERROR_GENERAL',
1229
                    'RealMe reported a serious application error. Please try again later. If the problem persists, ' .
1230
                        'please contact the RealMe Help Desk. From New Zealand: 0800 664 774 (toll free), from ' .
1231
                        'overseas dial +64 9 357 4468 (overseas call charges apply).'
1232
                );
1233
                break;
1234
        }
1235
1236
        // Allow message overrides if they exist
1237
        if (array_key_exists($errorCode, $messageOverrides) && !is_null($messageOverrides[$errorCode])) {
1238
            $message = $messageOverrides[$errorCode];
1239
        }
1240
1241
        return $message;
1242
    }
1243
}
1244