]> BookStack Code Mirror - bookstack/blob - tests/Auth/Saml2Test.php
Create additional test helper classes
[bookstack] / tests / Auth / Saml2Test.php
1 <?php
2
3 namespace Tests\Auth;
4
5 use BookStack\Auth\Role;
6 use BookStack\Auth\User;
7 use Tests\TestCase;
8
9 class Saml2Test extends TestCase
10 {
11     protected function setUp(): void
12     {
13         parent::setUp();
14         // Set default config for SAML2
15         config()->set([
16             'auth.method'                                   => 'saml2',
17             'auth.defaults.guard'                           => 'saml2',
18             'saml2.name'                                    => 'SingleSignOn-Testing',
19             'saml2.email_attribute'                         => 'email',
20             'saml2.display_name_attributes'                 => ['first_name', 'last_name'],
21             'saml2.external_id_attribute'                   => 'uid',
22             'saml2.user_to_groups'                          => false,
23             'saml2.group_attribute'                         => 'user_groups',
24             'saml2.remove_from_groups'                      => false,
25             'saml2.onelogin_overrides'                      => null,
26             'saml2.onelogin.idp.entityId'                   => 'http://saml.local/saml2/idp/metadata.php',
27             'saml2.onelogin.idp.singleSignOnService.url'    => 'http://saml.local/saml2/idp/SSOService.php',
28             'saml2.onelogin.idp.singleLogoutService.url'    => 'http://saml.local/saml2/idp/SingleLogoutService.php',
29             'saml2.autoload_from_metadata'                  => false,
30             'saml2.onelogin.idp.x509cert'                   => $this->testCert,
31             'saml2.onelogin.debug'                          => false,
32             'saml2.onelogin.security.requestedAuthnContext' => true,
33         ]);
34     }
35
36     public function test_metadata_endpoint_displays_xml_as_expected()
37     {
38         $req = $this->get('/saml2/metadata');
39         $req->assertHeader('Content-Type', 'text/xml; charset=UTF-8');
40         $req->assertSee('md:EntityDescriptor');
41         $req->assertSee(url('/saml2/acs'));
42     }
43
44     public function test_metadata_endpoint_loads_when_autoloading_with_bad_url_set()
45     {
46         config()->set([
47             'saml2.autoload_from_metadata' => true,
48             'saml2.onelogin.idp.entityId' => 'http://192.168.1.1:9292',
49             'saml2.onelogin.idp.singleSignOnService.url' => null,
50         ]);
51
52         $req = $this->get('/saml2/metadata');
53         $req->assertOk();
54         $req->assertHeader('Content-Type', 'text/xml; charset=UTF-8');
55         $req->assertSee('md:EntityDescriptor');
56     }
57
58     public function test_onelogin_overrides_functions_as_expected()
59     {
60         $json = '{"sp": {"assertionConsumerService": {"url": "https://example.com/super-cats"}}, "contactPerson": {"technical": {"givenName": "Barry Scott", "emailAddress": "[email protected]"}}}';
61         config()->set(['saml2.onelogin_overrides' => $json]);
62
63         $req = $this->get('/saml2/metadata');
64         $req->assertSee('https://example.com/super-cats');
65         $req->assertSee('md:ContactPerson');
66         $req->assertSee('<md:GivenName>Barry Scott</md:GivenName>', false);
67     }
68
69     public function test_login_option_shows_on_login_page()
70     {
71         $req = $this->get('/login');
72         $req->assertSeeText('SingleSignOn-Testing');
73         $this->withHtml($req)->assertElementExists('form[action$="/saml2/login"][method=POST] button');
74     }
75
76     public function test_login()
77     {
78         $req = $this->post('/saml2/login');
79         $redirect = $req->headers->get('location');
80         $this->assertStringStartsWith('http://saml.local/saml2/idp/SSOService.php', $redirect, 'Login redirects to SSO location');
81
82         config()->set(['saml2.onelogin.strict' => false]);
83         $this->assertFalse($this->isAuthenticated());
84
85         $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
86         $redirect = $acsPost->headers->get('Location');
87         $acsId = explode('?id=', $redirect)[1];
88         $this->assertTrue(strlen($acsId) > 12);
89
90         $this->assertStringContainsString('/saml2/acs?id=', $redirect);
91         $this->assertTrue(cache()->has('saml2_acs:' . $acsId));
92
93         $acsGet = $this->get($redirect);
94         $acsGet->assertRedirect('/');
95         $this->assertFalse(cache()->has('saml2_acs:' . $acsId));
96
97         $this->assertTrue($this->isAuthenticated());
98         $this->assertDatabaseHas('users', [
99             'email'            => '[email protected]',
100             'external_auth_id' => 'user',
101             'email_confirmed'  => false,
102             'name'             => 'Barry Scott',
103         ]);
104     }
105
106     public function test_acs_process_id_randomly_generated()
107     {
108         $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
109         $redirectA = $acsPost->headers->get('Location');
110
111         $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
112         $redirectB = $acsPost->headers->get('Location');
113
114         $this->assertFalse($redirectA === $redirectB);
115     }
116
117     public function test_process_acs_endpoint_cant_be_called_with_invalid_id()
118     {
119         $resp = $this->get('/saml2/acs');
120         $resp->assertRedirect('/login');
121         $this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
122
123         $resp = $this->get('/saml2/acs?id=abc123');
124         $resp->assertRedirect('/login');
125         $this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
126     }
127
128     public function test_group_role_sync_on_login()
129     {
130         config()->set([
131             'saml2.onelogin.strict'    => false,
132             'saml2.user_to_groups'     => true,
133             'saml2.remove_from_groups' => false,
134         ]);
135
136         $memberRole = Role::factory()->create(['external_auth_id' => 'member']);
137         $adminRole = Role::getSystemRole('admin');
138
139         $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
140         $user = User::query()->where('external_auth_id', '=', 'user')->first();
141
142         $userRoleIds = $user->roles()->pluck('id');
143         $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');
144         $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');
145     }
146
147     public function test_group_role_sync_removal_option_works_as_expected()
148     {
149         config()->set([
150             'saml2.onelogin.strict'    => false,
151             'saml2.user_to_groups'     => true,
152             'saml2.remove_from_groups' => true,
153         ]);
154
155         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
156         $user = User::query()->where('external_auth_id', '=', 'user')->first();
157
158         $randomRole = Role::factory()->create(['external_auth_id' => 'random']);
159         $user->attachRole($randomRole);
160         $this->assertContains($randomRole->id, $user->roles()->pluck('id'));
161
162         auth()->logout();
163         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
164         $this->assertNotContains($randomRole->id, $user->roles()->pluck('id'));
165     }
166
167     public function test_logout_link_directs_to_saml_path()
168     {
169         config()->set([
170             'saml2.onelogin.strict' => false,
171         ]);
172
173         $resp = $this->actingAs($this->users->editor())->get('/');
174         $this->withHtml($resp)->assertElementContains('form[action$="/saml2/logout"] button', 'Logout');
175     }
176
177     public function test_logout_sls_flow()
178     {
179         config()->set([
180             'saml2.onelogin.strict' => false,
181         ]);
182
183         $handleLogoutResponse = function () {
184             $this->assertTrue($this->isAuthenticated());
185
186             $req = $this->get('/saml2/sls');
187             $req->assertRedirect('/');
188             $this->assertFalse($this->isAuthenticated());
189         };
190
191         $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
192
193         $req = $this->post('/saml2/logout');
194         $redirect = $req->headers->get('location');
195         $this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);
196         $this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse);
197     }
198
199     public function test_logout_sls_flow_when_sls_not_configured()
200     {
201         config()->set([
202             'saml2.onelogin.strict'                      => false,
203             'saml2.onelogin.idp.singleLogoutService.url' => null,
204         ]);
205
206         $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
207         $this->assertTrue($this->isAuthenticated());
208
209         $req = $this->post('/saml2/logout');
210         $req->assertRedirect('/');
211         $this->assertFalse($this->isAuthenticated());
212     }
213
214     public function test_dump_user_details_option_works()
215     {
216         config()->set([
217             'saml2.onelogin.strict'   => false,
218             'saml2.dump_user_details' => true,
219         ]);
220
221         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
222         $acsPost->assertJsonStructure([
223             'id_from_idp',
224             'attrs_from_idp'      => [],
225             'attrs_after_parsing' => [],
226         ]);
227     }
228
229     public function test_saml_routes_are_only_active_if_saml_enabled()
230     {
231         config()->set(['auth.method' => 'standard']);
232         $getRoutes = ['/metadata', '/sls'];
233         foreach ($getRoutes as $route) {
234             $req = $this->get('/saml2' . $route);
235             $this->assertPermissionError($req);
236         }
237
238         $postRoutes = ['/login', '/acs', '/logout'];
239         foreach ($postRoutes as $route) {
240             $req = $this->post('/saml2' . $route);
241             $this->assertPermissionError($req);
242         }
243     }
244
245     public function test_forgot_password_routes_inaccessible()
246     {
247         $resp = $this->get('/password/email');
248         $this->assertPermissionError($resp);
249
250         $resp = $this->post('/password/email');
251         $this->assertPermissionError($resp);
252
253         $resp = $this->get('/password/reset/abc123');
254         $this->assertPermissionError($resp);
255
256         $resp = $this->post('/password/reset');
257         $this->assertPermissionError($resp);
258     }
259
260     public function test_standard_login_routes_inaccessible()
261     {
262         $resp = $this->post('/login');
263         $this->assertPermissionError($resp);
264
265         $resp = $this->post('/logout');
266         $this->assertPermissionError($resp);
267     }
268
269     public function test_user_invite_routes_inaccessible()
270     {
271         $resp = $this->get('/register/invite/abc123');
272         $this->assertPermissionError($resp);
273
274         $resp = $this->post('/register/invite/abc123');
275         $this->assertPermissionError($resp);
276     }
277
278     public function test_user_register_routes_inaccessible()
279     {
280         $resp = $this->get('/register');
281         $this->assertPermissionError($resp);
282
283         $resp = $this->post('/register');
284         $this->assertPermissionError($resp);
285     }
286
287     public function test_email_domain_restriction_active_on_new_saml_login()
288     {
289         $this->setSettings([
290             'registration-restrict' => 'testing.com',
291         ]);
292         config()->set([
293             'saml2.onelogin.strict' => false,
294         ]);
295
296         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
297         $acsPost->assertSeeText('That email domain does not have access to this application');
298         $this->assertFalse(auth()->check());
299         $this->assertDatabaseMissing('users', ['email' => '[email protected]']);
300     }
301
302     public function test_group_sync_functions_when_email_confirmation_required()
303     {
304         setting()->put('registration-confirmation', 'true');
305         config()->set([
306             'saml2.onelogin.strict'    => false,
307             'saml2.user_to_groups'     => true,
308             'saml2.remove_from_groups' => false,
309         ]);
310
311         $memberRole = Role::factory()->create(['external_auth_id' => 'member']);
312         $adminRole = Role::getSystemRole('admin');
313
314         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
315
316         $this->assertEquals('http://localhost/register/confirm', url()->current());
317         $acsPost->assertSee('Please check your email and click the confirmation button to access BookStack.');
318         /** @var User $user */
319         $user = User::query()->where('external_auth_id', '=', 'user')->first();
320
321         $userRoleIds = $user->roles()->pluck('id');
322         $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');
323         $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');
324         $this->assertFalse(boolval($user->email_confirmed), 'User email remains unconfirmed');
325
326         $this->assertNull(auth()->user());
327         $homeGet = $this->get('/');
328         $homeGet->assertRedirect('/login');
329     }
330
331     public function test_login_where_existing_non_saml_user_shows_warning()
332     {
333         $this->post('/saml2/login');
334         config()->set(['saml2.onelogin.strict' => false]);
335
336         // Make the user pre-existing in DB with different auth_id
337         User::query()->forceCreate([
338             'email'            => '[email protected]',
339             'external_auth_id' => 'old_system_user_id',
340             'email_confirmed'  => false,
341             'name'             => 'Barry Scott',
342         ]);
343
344         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
345         $this->assertFalse($this->isAuthenticated());
346         $this->assertDatabaseHas('users', [
347             'email'            => '[email protected]',
348             'external_auth_id' => 'old_system_user_id',
349         ]);
350
351         $acsPost->assertSee('A user with the email [email protected] already exists but with different credentials');
352     }
353
354     public function test_login_request_contains_expected_default_authncontext()
355     {
356         $authReq = $this->getAuthnRequest();
357         $this->assertStringContainsString('samlp:RequestedAuthnContext Comparison="exact"', $authReq);
358         $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>', $authReq);
359     }
360
361     public function test_false_idp_authncontext_option_does_not_pass_authncontext_in_saml_request()
362     {
363         config()->set(['saml2.onelogin.security.requestedAuthnContext' => false]);
364         $authReq = $this->getAuthnRequest();
365         $this->assertStringNotContainsString('samlp:RequestedAuthnContext', $authReq);
366         $this->assertStringNotContainsString('<saml:AuthnContextClassRef>', $authReq);
367     }
368
369     public function test_array_idp_authncontext_option_passes_value_as_authncontextclassref_in_request()
370     {
371         config()->set(['saml2.onelogin.security.requestedAuthnContext' => ['urn:federation:authentication:windows', 'urn:federation:authentication:linux']]);
372         $authReq = $this->getAuthnRequest();
373         $this->assertStringContainsString('samlp:RequestedAuthnContext', $authReq);
374         $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:windows</saml:AuthnContextClassRef>', $authReq);
375         $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:linux</saml:AuthnContextClassRef>', $authReq);
376     }
377
378     protected function getAuthnRequest(): string
379     {
380         $req = $this->post('/saml2/login');
381         $location = $req->headers->get('Location');
382         $query = explode('?', $location)[1];
383         $params = [];
384         parse_str($query, $params);
385
386         return gzinflate(base64_decode($params['SAMLRequest']));
387     }
388
389     protected function withGet(array $options, callable $callback)
390     {
391         return $this->withGlobal($_GET, $options, $callback);
392     }
393
394     protected function withGlobal(array &$global, array $options, callable $callback)
395     {
396         $original = [];
397         foreach ($options as $key => $val) {
398             $original[$key] = $global[$key] ?? null;
399             $global[$key] = $val;
400         }
401
402         $callback();
403
404         foreach ($options as $key => $val) {
405             $val = $original[$key];
406             if ($val) {
407                 $global[$key] = $val;
408             } else {
409                 unset($global[$key]);
410             }
411         }
412     }
413
414     /**
415      * The post data for a callback for single-sign-in.
416      * Provides the following attributes:
417      * array:5 [
418      * "uid" => array:1 [
419      * 0 => "user"
420      * ]
421      * "first_name" => array:1 [
422      * 0 => "Barry"
423      * ]
424      * "last_name" => array:1 [
425      * 0 => "Scott"
426      * ]
427      * "email" => array:1 [
428      * 0 => "[email protected]"
429      * ]
430      * "user_groups" => array:2 [
431      * 0 => "member"
432      * 1 => "admin"
433      * ]
434      * ].
435      */
436     protected $acsPostData = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxOS0xMS0xN1QxNzo1MzozOVoiIERlc3RpbmF0aW9uPSJodHRwOi8vYm9va3N0YWNrLmxvY2FsL3NhbWwyL2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82YTBmNGYzOTkzMDQwZjE5ODdmZDM3MDY4YjUyOTYyMjlhZDUzNjFjIj48c2FtbDpJc3N1ZXI+aHR0cDovL3NhbWwubG9jYWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+dm1oL1M3NU5mK2crZWNESkN6QWJaV0tKVmx1ZzdCZnNDKzlhV05lSXJlUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dnJhZ0tKWHNjVm5UNjJFaEk3bGk4MERUWHNOTGJOc3lwNWZ2QnU4WjFYSEtFUVA3QWpPNkcxcVBwaGpWQ2dRMzd6TldVVTZvUytQeFA3UDlHeG5xL3hKejRUT3lHcHJ5N1RoK2pIcHc0YWVzQTdrTmp6VU51UmU2c1ltWTlrRXh2VjMvTmJRZjROMlM2Y2RhRHIzWFRodllVVDcxYzQwNVVHOFJpQjJaY3liWHIxZU1yWCtXUDBnU2Qrc0F2RExqTjBJc3pVWlVUNThadFpEVE1ya1ZGL0pIbFBFQ04vVW1sYVBBeitTcUJ4c25xTndZK1oxYUt3MnlqeFRlNnUxM09Kb29OOVN1REowNE0rK2F3RlY3NkI4cXEyTzMxa3FBbDJibm1wTGxtTWdRNFEraUlnL3dCc09abTV1clphOWJObDNLVEhtTVBXbFpkbWhsLzgvMy9IT1RxN2thWGs3cnlWRHRLcFlsZ3FUajNhRUpuL0dwM2o4SFp5MUVialRiOTRRT1ZQMG5IQzB1V2hCaE13TjdzVjFrUSsxU2NjUlpUZXJKSGlSVUQvR0srTVg3M0YrbzJVTFRIL1Z6Tm9SM2o4N2hOLzZ1UC9JeG5aM1RudGR1MFZPZS9ucEdVWjBSMG9SWFhwa2JTL2poNWk1ZjU0RXN4eXZ1VEM5NHdKaEM8L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlFYXpDQ0F0T2dBd0lCQWdJVWU3YTA4OENucjRpem1ybkJFbng1cTNIVE12WXdEUVlKS29aSWh2Y05BUUVMQlFBd1JURUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTUdFbHVkR1Z5Ym1WMElGZHBaR2RwZEhNZ1VIUjVJRXgwWkRBZUZ3MHhPVEV4TVRZeE1qRTNNVFZhRncweU9URXhNVFV4TWpFM01UVmFNRVV4Q3pBSkJnTlZCQVlUQWtkQ01STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FEekxlOUZmZHlwbFR4SHA0U3VROWdRdFpUM3QrU0RmdkVMNzJwcENmRlp3NytCNXM1Qi9UNzNhWHBvUTNTNTNwR0kxUklXQ2dlMmlDVVEydHptMjdhU05IMGl1OWFKWWNVUVovUklUcWQwYXl5RGtzMU5BMlBUM1RXNnQzbTdLVjVyZTRQME5iK1lEZXV5SGRreitqY010cG44Q21Cb1QwSCtza2hhMGhpcUlOa2prUlBpSHZMSFZHcCt0SFVFQS9JNm1ONGFCL1VFeFNUTHM3OU5zTFVmdGVxcXhlOSt0dmRVYVRveURQcmhQRmpPTnMrOU5LQ2t6SUM2dmN2N0o2QXR1S0c2bkVUK3pCOXlPV2d0R1lRaWZYcVFBMnk1ZEw4MUJCMHE1dU1hQkxTMnBxM2FQUGp6VTJGMytFeXNqeVNXVG5Da2ZrN0M1U3NDWFJ1OFErVTk1dHVucE5md2Y1b2xFNldhczQ4Tk1NK1B3VjdpQ05NUGtOemxscTZQQ2lNK1A4RHJNU2N6elVaWlFVU3Y2ZFN3UENvK1lTVmltRU0wT2czWEpUaU5oUTVBTmxhSW42Nkt3NWdmb0JmdWlYbXlJS2lTRHlBaURZbUZhZjQzOTV3V3dMa1RSK2N3OFdmamFIc3dLWlRvbW4xTVIzT0pzWTJVSjBlUkJZTStZU3NDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01COEdBMVVkSXdRWU1CYUFGSW1wMkNZQ0dmY2I3dzkxSC9jU2hUQ2tYd1IvTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0dCQUErZy9DN3VMOWxuK1crcUJrbkxXODFrb2pZZmxnUEsxSTFNSEl3bk12bC9aVEhYNGRSWEtEcms3S2NVcTFLanFhak5WNjZmMWNha3AwM0lpakJpTzBYaTFnWFVaWUxvQ2lOR1V5eXA5WGxvaUl5OVh3MlBpV25ydzAreVp5dlZzc2JlaFhYWUpsNFJpaEJqQld1bDlSNHdNWUxPVVNKRGUyV3hjVUJoSm54eU5ScytQMHhMU1FYNkIybjZueG9Ea280cDA3czhaS1hRa2VpWjJpd0ZkVHh6UmtHanRoTVV2NzA0bnpzVkdCVDBEQ1B0ZlNhTzVLSlpXMXJDczN5aU10aG5CeHE0cUVET1FKRklsKy9MRDcxS2JCOXZaY1c1SnVhdnpCRm1rS0dOcm8vNkcxSTdlbDQ2SVI0d2lqVHlORkNZVXVEOWR0aWduTm1wV3ROOE9XK3B0aUwvanRUeVNXdWtqeXMwcyt2TG44M0NWdmpCMGRKdFZBSVlPZ1hGZEl1aWk2Nmdjend3TS9MR2lPRXhKbjBkVE56c0ovSVlocHhMNEZCRXVQMHBza1kwbzBhVWxKMkxTMmord1NRVFJLc0JnTWp5clVyZWtsZTJPRFN0U3RuM2VhYmpJeDAvRkhscEZyMGpOSW0vb01QN2t3anRVWDR6YU5lNDdRSTRHZz09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il82ODQyZGY5YzY1OWYxM2ZlNTE5NmNkOWVmNmMyZjAyODM2NGFlOTQzYjEiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE5LTExLTE3VDE3OjUzOjM5WiI+PHNhbWw6SXNzdWVyPmh0dHA6Ly9zYW1sLmxvY2FsL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjXzY4NDJkZjljNjU5ZjEzZmU1MTk2Y2Q5ZWY2YzJmMDI4MzY0YWU5NDNiMSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPmtyYjV3NlM4dG9YYy9lU3daUFVPQnZRem4zb3M0SkFDdXh4ckpreHBnRnc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPjJxcW1Ba3hucXhOa3N5eXh5dnFTVDUxTDg5VS9ZdHpja2t1ekF4ci9hQ1JTK1NPRzg1YkFNWm8vU3puc3d0TVlBYlFRQ0VGb0R1amdNdlpzSFl3NlR2dmFHanlXWUpRNVZyYWhlemZaSWlCVUU0NHBtWGFrOCswV0l0WTVndnBGSXhxWFZaRmdFUkt2VExmZVFCMzhkMVZQc0ZVZ0RYdXQ4VS9Qdm43dXZwdXZjVXorMUUyOUVKR2FZL0dndnhUN0tyWU9SQTh3SitNdVRzUVZtanNlUnhveVJTejA4TmJ3ZTJIOGpXQnpFWWNxWWwyK0ZnK2hwNWd0S216VmhLRnBkNXZBNjdBSXo1NXN0QmNHNSswNHJVaWpFSzRzci9xa0x5QmtKQjdLdkwzanZKcG8zQjhxYkxYeXhLb1dSSmRnazhKNHMvTVp1QWk3QWUxUXNTTjl2Z3ZTdVRlc0VCUjVpSHJuS1lrbEpRWXNrbUQzbSsremE4U1NRbnBlM0UzYUZBY3p6cElUdUQ4YkFCWmRqcUk2TkhrSmFRQXBmb0hWNVQrZ244ejdUTWsrSStUU2JlQURubUxCS3lnMHRabW10L0ZKbDV6eWowVmxwc1dzTVM2OVE2bUZJVStqcEhSanpOb2FLMVM1dlQ3ZW1HbUhKSUp0cWlOdXJRN0tkQlBJPC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRWF6Q0NBdE9nQXdJQkFnSVVlN2EwODhDbnI0aXptcm5CRW54NXEzSFRNdll3RFFZSktvWklodmNOQVFFTEJRQXdSVEVMTUFrR0ExVUVCaE1DUjBJeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWVGdzB4T1RFeE1UWXhNakUzTVRWYUZ3MHlPVEV4TVRVeE1qRTNNVFZhTUVVeEN6QUpCZ05WQkFZVEFrZENNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3Z2dHaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQmp3QXdnZ0dLQW9JQmdRRHpMZTlGZmR5cGxUeEhwNFN1UTlnUXRaVDN0K1NEZnZFTDcycHBDZkZadzcrQjVzNUIvVDczYVhwb1EzUzUzcEdJMVJJV0NnZTJpQ1VRMnR6bTI3YVNOSDBpdTlhSlljVVFaL1JJVHFkMGF5eURrczFOQTJQVDNUVzZ0M203S1Y1cmU0UDBOYitZRGV1eUhka3oramNNdHBuOENtQm9UMEgrc2toYTBoaXFJTmtqa1JQaUh2TEhWR3ArdEhVRUEvSTZtTjRhQi9VRXhTVExzNzlOc0xVZnRlcXF4ZTkrdHZkVWFUb3lEUHJoUEZqT05zKzlOS0NreklDNnZjdjdKNkF0dUtHNm5FVCt6Qjl5T1dndEdZUWlmWHFRQTJ5NWRMODFCQjBxNXVNYUJMUzJwcTNhUFBqelUyRjMrRXlzanlTV1RuQ2tmazdDNVNzQ1hSdThRK1U5NXR1bnBOZndmNW9sRTZXYXM0OE5NTStQd1Y3aUNOTVBrTnpsbHE2UENpTStQOERyTVNjenpVWlpRVVN2NmRTd1BDbytZU1ZpbUVNME9nM1hKVGlOaFE1QU5sYUluNjZLdzVnZm9CZnVpWG15SUtpU0R5QWlEWW1GYWY0Mzk1d1d3TGtUUitjdzhXZmphSHN3S1pUb21uMU1SM09Kc1kyVUowZVJCWU0rWVNzQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZJbXAyQ1lDR2ZjYjd3OTFIL2NTaFRDa1h3Ui9NQjhHQTFVZEl3UVlNQmFBRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dHQkFBK2cvQzd1TDlsbitXK3FCa25MVzgxa29qWWZsZ1BLMUkxTUhJd25NdmwvWlRIWDRkUlhLRHJrN0tjVXExS2pxYWpOVjY2ZjFjYWtwMDNJaWpCaU8wWGkxZ1hVWllMb0NpTkdVeXlwOVhsb2lJeTlYdzJQaVducncwK3laeXZWc3NiZWhYWFlKbDRSaWhCakJXdWw5UjR3TVlMT1VTSkRlMld4Y1VCaEpueHlOUnMrUDB4TFNRWDZCMm42bnhvRGtvNHAwN3M4WktYUWtlaVoyaXdGZFR4elJrR2p0aE1VdjcwNG56c1ZHQlQwRENQdGZTYU81S0paVzFyQ3MzeWlNdGhuQnhxNHFFRE9RSkZJbCsvTEQ3MUtiQjl2WmNXNUp1YXZ6QkZta0tHTnJvLzZHMUk3ZWw0NklSNHdpalR5TkZDWVV1RDlkdGlnbk5tcFd0TjhPVytwdGlML2p0VHlTV3VranlzMHMrdkxuODNDVnZqQjBkSnRWQUlZT2dYRmRJdWlpNjZnY3p3d00vTEdpT0V4Sm4wZFROenNKL0lZaHB4TDRGQkV1UDBwc2tZMG8wYVVsSjJMUzJqK3dTUVRSS3NCZ01qeXJVcmVrbGUyT0RTdFN0bjNlYWJqSXgwL0ZIbHBGcjBqTkltL29NUDdrd2p0VVg0emFOZTQ3UUk0R2c9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiPl8yYzdhYjg2ZWI4ZjFkMTA2MzQ0M2YyMTljYzU4NjhmZjY2NzA4OTEyZTM8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMTktMTEtMTdUMTc6NTg6MzlaIiBSZWNpcGllbnQ9Imh0dHA6Ly9ib29rc3RhY2subG9jYWwvc2FtbDIvYWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzZhMGY0ZjM5OTMwNDBmMTk4N2ZkMzcwNjhiNTI5NjIyOWFkNTM2MWMiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxOS0xMS0xN1QxNzo1MzowOVoiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMS0xN1QxNzo1ODozOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTktMTEtMTdUMTc6NTM6MzlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE5LTExLTE4VDAxOjUzOjM5WiIgU2Vzc2lvbkluZGV4PSJfNGZlN2MwZDE1NzJkNjRiMjdmOTMwYWE2ZjIzNmE2ZjQyZTkzMDkwMWNjIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZmlyc3RfbmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+QmFycnk8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibGFzdF9uYW1lIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5TY290dDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlbWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1c2VyX2dyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+bWVtYmVyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+';
437
438     protected $sloResponseData = 'fZHRa8IwEMb/lZJ3bdJa04a2MOYYglOY4sNe5JKms9gmpZfC/vxF3ZjC8OXgLvl938ddjtC1vVjZTzu6d429NaiDr641KC5PBRkHIyxgg8JAp1E4JbZPbysRTanoB+ussi25QR4TgKgH11hDguWiIIeawTxOaK1iPYt5XcczHUlJeVRlMklBJjOuM1qDVCTY6wE9WRAv5HHEUS8NOjDOjyjLJoxNGN+xVESpSNgHCRYaXWPAXaijc70IQ2ntyUPqNG2tgjY8Z45CbNFLmt8V7GxBNuuX1eZ1uT7EcZJKAE4TJhXPaMxlVlFffPKKJnXE5ryusoiU+VlMXJIN5Y/feXRn1VR92GkHFTiY9sc+D2+p/HqRrQM34n33bCsd7KEd9eMd4+W32I5KaUQSlleHP9Hwv6uX3w==';
439
440     protected $testCert = 'MIIEazCCAtOgAwIBAgIUe7a088Cnr4izmrnBEnx5q3HTMvYwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTExMTYxMjE3MTVaFw0yOTExMTUxMjE3MTVaMEUxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDzLe9FfdyplTxHp4SuQ9gQtZT3t+SDfvEL72ppCfFZw7+B5s5B/T73aXpoQ3S53pGI1RIWCge2iCUQ2tzm27aSNH0iu9aJYcUQZ/RITqd0ayyDks1NA2PT3TW6t3m7KV5re4P0Nb+YDeuyHdkz+jcMtpn8CmBoT0H+skha0hiqINkjkRPiHvLHVGp+tHUEA/I6mN4aB/UExSTLs79NsLUfteqqxe9+tvdUaToyDPrhPFjONs+9NKCkzIC6vcv7J6AtuKG6nET+zB9yOWgtGYQifXqQA2y5dL81BB0q5uMaBLS2pq3aPPjzU2F3+EysjySWTnCkfk7C5SsCXRu8Q+U95tunpNfwf5olE6Was48NMM+PwV7iCNMPkNzllq6PCiM+P8DrMSczzUZZQUSv6dSwPCo+YSVimEM0Og3XJTiNhQ5ANlaIn66Kw5gfoBfuiXmyIKiSDyAiDYmFaf4395wWwLkTR+cw8WfjaHswKZTomn1MR3OJsY2UJ0eRBYM+YSsCAwEAAaNTMFEwHQYDVR0OBBYEFImp2CYCGfcb7w91H/cShTCkXwR/MB8GA1UdIwQYMBaAFImp2CYCGfcb7w91H/cShTCkXwR/MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggGBAA+g/C7uL9ln+W+qBknLW81kojYflgPK1I1MHIwnMvl/ZTHX4dRXKDrk7KcUq1KjqajNV66f1cakp03IijBiO0Xi1gXUZYLoCiNGUyyp9XloiIy9Xw2PiWnrw0+yZyvVssbehXXYJl4RihBjBWul9R4wMYLOUSJDe2WxcUBhJnxyNRs+P0xLSQX6B2n6nxoDko4p07s8ZKXQkeiZ2iwFdTxzRkGjthMUv704nzsVGBT0DCPtfSaO5KJZW1rCs3yiMthnBxq4qEDOQJFIl+/LD71KbB9vZcW5JuavzBFmkKGNro/6G1I7el46IR4wijTyNFCYUuD9dtignNmpWtN8OW+ptiL/jtTySWukjys0s+vLn83CVvjB0dJtVAIYOgXFdIuii66gczwwM/LGiOExJn0dTNzsJ/IYhpxL4FBEuP0pskY0o0aUlJ2LS2j+wSQTRKsBgMjyrUrekle2ODStStn3eabjIx0/FHlpFr0jNIm/oMP7kwjtUX4zaNe47QI4Gg==';
441 }