Passed
Push — master ( 9b983a...31893a )
by
unknown
02:23
created

RealMeService::getRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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