Completed
Pull Request — master (#40)
by Indy
06:50 queued 04:59
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-----\n([^-]*)\n-----END [A-Z ]*PRIVATE KEY-----/';
739
                } else {
740
                    throw new InvalidArgumentException('Argument contentType must be either "certificate" or "key"');
741
                }
742
743
                // This is a PEM key, and we need to extract just the certificate, stripping out the private key etc.
744
                // So we search for everything between '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----'
745
                preg_match(
746
                    $pattern,
747
                    $certificateContents,
748
                    $matches
749
                );
750
751
                if (isset($matches) && is_array($matches) && isset($matches[1])) {
752
                    $text = trim($matches[1]);
753
                }
754
            }
755
        }
756
757
        return $text;
758
    }
759
760
    /**
761
     * @param string $env The environment to return the entity ID for. Must be one of the RealMe environment names
762
     * @return string|null Either the assertion consumer service location, or null if information doesn't exist
763
     */
764
    public function getAssertionConsumerServiceUrlForEnvironment($env)
765
    {
766
        if (in_array($env, $this->getAllowedRealMeEnvironments()) === false) {
767
            return null;
768
        }
769
770
        $domain = $this->getMetadataAssertionServiceDomainForEnvironment($env);
771
        if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
772
            return null;
773
        }
774
775
        // Returns https://domain.govt.nz/Security/login/RealMe/acs
776
        return Controller::join_links($domain, Security::config()->get('login_url'), 'RealMe/acs');
777
    }
778
779
    /**
780
     * @return string|null The organisation name to be used in metadata XML output, or null if none exists
781
     */
782
    public function getMetadataOrganisationName()
783
    {
784
        $orgName = $this->config()->metadata_organisation_name;
785
        return (strlen($orgName) > 0) ? $orgName : null;
786
    }
787
788
    /**
789
     * @return string|null The organisation display name to be used in metadata XML output, or null if none exists
790
     */
791
    public function getMetadataOrganisationDisplayName()
792
    {
793
        $displayName = $this->config()->metadata_organisation_display_name;
794
        return (strlen($displayName) > 0) ? $displayName : null;
795
    }
796
797
    /**
798
     * @return string|null The organisation website URL to be used in metadata XML output, or null if none exists
799
     */
800
    public function getMetadataOrganisationUrl()
801
    {
802
        $url = $this->config()->metadata_organisation_url;
803
        return (strlen($url) > 0) ? $url: null;
804
    }
805
806
    /**
807
     * @return string[] The support contact details to be used in metadata XML output, with null values if they don't
808
     *                  exist
809
     */
810
    public function getMetadataContactSupport()
811
    {
812
        $company = $this->config()->metadata_contact_support_company;
813
        $firstNames = $this->config()->metadata_contact_support_firstnames;
814
        $surname = $this->config()->metadata_contact_support_surname;
815
816
        return array(
817
            'company' => (strlen($company) > 0) ? $company : null,
818
            'firstNames' => (strlen($firstNames) > 0) ? $firstNames : null,
819
            'surname' => (strlen($surname) > 0) ? $surname : null
820
        );
821
    }
822
823
    /**
824
     * The list of RealMe environments that can be used. By default, we allow mts, ite and production.
825
     * @return array
826
     */
827
    public function getAllowedRealMeEnvironments()
828
    {
829
        return $this->config()->allowed_realme_environments;
830
    }
831
832
    /**
833
     * The list of valid realme AuthNContexts
834
     * @return array
835
     */
836
    public function getAllowedAuthNContextList()
837
    {
838
        return $this->config()->allowed_authn_context_list;
839
    }
840
841
    /**
842
     * Returns the appropriate entity ID for RealMe, given the environment passed in. The entity ID may be different per
843
     * environment, and should be a full URL, including privacy realm and application name. For example, this may be:
844
     * https://www.agency.govt.nz/privacy-realm-name/application-name
845
     *
846
     * @return string|null Returns the entity ID for the current environment, or null if no entity ID exists
847
     */
848
    public function getSPEntityID()
849
    {
850
        return $this->getConfigurationVarByEnv('sp_entity_ids', $this->config()->realme_env);
851
    }
852
853
    private function getIdPEntityID()
854
    {
855
        $cfg = $this->config();
856
        return $this->getConfigurationVarByEnv('idp_entity_ids', $cfg->realme_env, $cfg->integration_type);
857
    }
858
859
    private function getSingleSignOnServiceURL()
860
    {
861
        $cfg = $this->config();
862
        return $this->getConfigurationVarByEnv('idp_sso_service_urls', $cfg->realme_env, $cfg->integration_type);
863
    }
864
865
    private function getRequestedAuthnContext()
866
    {
867
        return $this->getConfigurationVarByEnv('authn_contexts', $this->config()->realme_env);
868
    }
869
870
    /**
871
     * Returns the internal {@link OneLogin_Saml2_Auth} object against which visitors are authenticated.
872
     *
873
     * @return OneLogin_Saml2_Auth
874
     */
875
    public function getAuth(HTTPRequest $request = null)
876
    {
877
        if (isset($this->auth)) {
878
            return $this->auth;
879
        }
880
881
        if (!$request) {
882
            $request = self::getRequest();
883
            if (!$request) {
884
                throw new RealMeException('A request must be provided for session access');
885
            }
886
        }
887
888
        // Ensure onelogin is using the correct host, protocol and port incase a proxy is involved
889
        OneLogin_Saml2_Utils::setSelfHost($request->getHeader('Host'));
890
        OneLogin_Saml2_Utils::setSelfProtocol($request->getScheme());
891
892
        $port = null;
893
        if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) {
894
            $port = $_SERVER['HTTP_X_FORWARDED_PORT'];
895
        } elseif (isset($_SERVER['SERVER_PORT'])) {
896
            $port = $_SERVER['SERVER_PORT'];
897
        }
898
899
        if ($port) {
900
            OneLogin_Saml2_Utils::setSelfPort($port);
901
        }
902
903
        $settings = [
904
            'strict' => true,
905
            'debug' => false,
906
907
            // Service Provider (this installation) configuration
908
            'sp' => [
909
                'entityId' => $this->getSPEntityID(),
910
                'x509cert' => $this->getSPCertContent('certificate'),
911
                'privateKey' => $this->getSPCertContent('key'),
912
913
                // According to RealMe messaging spec, must always be transient for assert; is irrelevant for login
914
                'NameIDFormat' => $this->getNameIdFormat(),
915
916
                'assertionConsumerService' => [
917
                    'url' => $this->getAssertionConsumerServiceUrlForEnvironment($this->config()->realme_env),
918
                    'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' // Always POST, not artifact binding
919
                ]
920
            ],
921
922
            // RealMe Identity Provider configuration
923
            'idp' => [
924
                'entityId' => $this->getIdPEntityID(),
925
                'x509cert' => $this->getIdPCertContent(),
926
927
                'singleSignOnService' => [
928
                    'url' => $this->getSingleSignOnServiceURL(),
929
                    'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
930
                ]
931
            ],
932
933
            'security' => [
934
                'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
935
                'authnRequestsSigned' => true,
936
                'wantAssertionsEncrypted' => true,
937
                'wantAssertionsSigned' => true,
938
939
                'requestedAuthnContext' => [
940
                    $this->getRequestedAuthnContext()
941
                ]
942
            ]
943
        ];
944
945
        $this->auth = new OneLogin_Saml2_Auth($settings);
946
        return $this->auth;
947
    }
948
949
    /**
950
     * @return string the required NameIDFormat to be included in metadata XML, based on the requested integration type
951
     */
952
    public function getNameIdFormat()
953
    {
954
        switch ($this->config()->integration_type) {
955
            case self::TYPE_ASSERT:
956
                return 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient';
957
                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...
958
959
            case self::TYPE_LOGIN:
960
            default:
961
                return 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent';
962
                break;
963
        }
964
    }
965
966
    /**
967
     * @param string $cfgName The static configuration value to get. This should be an array
968
     * @param string $env The environment to return the value for. Must be one of the RealMe environment names
969
     * @param string $integrationType The integration type (login or assert), if necessary, to determine return var
970
     * @throws InvalidArgumentException If the cfgVar doesn't exist, or is malformed
971
     * @return string|null Returns the value as defined in $cfgName for the given environment, or null if none exist
972
     */
973
    private function getConfigurationVarByEnv($cfgName, $env, $integrationType = null)
974
    {
975
        $value = null;
976
977
        if (in_array($env, $this->getAllowedRealMeEnvironments())) {
978
            $values = $this->config()->$cfgName;
979
980
            if (is_array($values) && isset($values[$env])) {
981
                $value = $values[$env];
982
            }
983
        }
984
985
        // If $integrationType is specified, then $value should be an array, with the array key being the integration
986
        // type and array value being the returned variable
987
        if (!is_null($integrationType) && is_array($value) && isset($value[$integrationType])) {
988
            $value = $value[$integrationType];
989
        } elseif (!is_null($integrationType)) {
990
            // Otherwise, we are expecting an integration type, but the value is not specified that way, error out
991
            throw new InvalidArgumentException(
992
                sprintf(
993
                    'Config value %s[%s][%s] not well formed (cfg var not an array)',
994
                    $cfgName,
995
                    $env,
996
                    $integrationType
997
                )
998
            );
999
        }
1000
1001
        if (is_null($value)) {
1002
            throw new InvalidArgumentException(sprintf('Config value %s[%s] not set', $cfgName, $env));
1003
        }
1004
1005
        return $value;
1006
    }
1007
1008
    /**
1009
     * @param string $certName The certificate name, either 'SIGNING' or 'MUTUAL'
1010
     * @return string|null Either the full path to the certificate file, or null if it doesn't exist
1011
     * @see self::getSigningCertPath()
1012
     */
1013
    private function getCertPath($certName)
1014
    {
1015
        $certPath = null;
1016
1017
        if (in_array($certName, array('SIGNING', 'MUTUAL'))) {
1018
            $constName = sprintf('REALME_%s_CERT_FILENAME', strtoupper($certName));
1019
            if ($filename = Environment::getEnv($constName)) {
1020
                $certPath = $this->getCertDir($filename);
1021
            }
1022
        }
1023
1024
        // Ensure the file exists, if it doesn't then set it to null
1025
        if (!is_null($certPath) && !file_exists($certPath)) {
1026
            $certPath = null;
1027
        }
1028
1029
        return $certPath;
1030
    }
1031
1032
    /**
1033
     * @param string $env The environment to return the domain name for. Must be one of the RealMe environment names
1034
     * @return string|null Either the FQDN (e.g. https://www.realme-demo.govt.nz/) or null if none is specified
1035
     */
1036
    private function getMetadataAssertionServiceDomainForEnvironment($env)
1037
    {
1038
        return $this->getConfigurationVarByEnv('metadata_assertion_service_domains', $env);
1039
    }
1040
1041
    /**
1042
     * @param OneLogin_Saml2_Auth $auth
1043
     * @return string|null null if there's no FLT, or a string if there is one
1044
     */
1045
    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

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