BookStack/tests/Auth/Saml2Test.php

428 lines
29 KiB
PHP
Raw Normal View History

2021-06-26 23:23:15 +08:00
<?php
namespace Tests\Auth;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use Tests\TestCase;
class Saml2Test extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Set default config for SAML2
config()->set([
2021-06-26 23:23:15 +08:00
'auth.method' => 'saml2',
'auth.defaults.guard' => 'saml2',
'saml2.name' => 'SingleSignOn-Testing',
'saml2.email_attribute' => 'email',
'saml2.display_name_attributes' => ['first_name', 'last_name'],
'saml2.external_id_attribute' => 'uid',
'saml2.user_to_groups' => false,
'saml2.group_attribute' => 'user_groups',
'saml2.remove_from_groups' => false,
'saml2.onelogin_overrides' => null,
'saml2.onelogin.idp.entityId' => 'http://saml.local/saml2/idp/metadata.php',
'saml2.onelogin.idp.singleSignOnService.url' => 'http://saml.local/saml2/idp/SSOService.php',
'saml2.onelogin.idp.singleLogoutService.url' => 'http://saml.local/saml2/idp/SingleLogoutService.php',
'saml2.autoload_from_metadata' => false,
'saml2.onelogin.idp.x509cert' => $this->testCert,
'saml2.onelogin.debug' => false,
'saml2.onelogin.security.requestedAuthnContext' => true,
]);
}
public function test_metadata_endpoint_displays_xml_as_expected()
{
$req = $this->get('/saml2/metadata');
$req->assertHeader('Content-Type', 'text/xml; charset=UTF-8');
$req->assertSee('md:EntityDescriptor');
$req->assertSee(url('/saml2/acs'));
}
public function test_onelogin_overrides_functions_as_expected()
{
$json = '{"sp": {"assertionConsumerService": {"url": "https://example.com/super-cats"}}, "contactPerson": {"technical": {"givenName": "Barry Scott", "emailAddress": "barry@example.com"}}}';
config()->set(['saml2.onelogin_overrides' => $json]);
$req = $this->get('/saml2/metadata');
$req->assertSee('https://example.com/super-cats');
$req->assertSee('md:ContactPerson');
$req->assertSee('<md:GivenName>Barry Scott</md:GivenName>', false);
}
public function test_login_option_shows_on_login_page()
{
$req = $this->get('/login');
$req->assertSeeText('SingleSignOn-Testing');
$req->assertElementExists('form[action$="/saml2/login"][method=POST] button');
}
public function test_login()
{
$req = $this->post('/saml2/login');
$redirect = $req->headers->get('location');
$this->assertStringStartsWith('http://saml.local/saml2/idp/SSOService.php', $redirect, 'Login redirects to SSO location');
config()->set(['saml2.onelogin.strict' => false]);
$this->assertFalse($this->isAuthenticated());
$acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$redirect = $acsPost->headers->get('Location');
$acsId = explode('?id=', $redirect)[1];
$this->assertTrue(strlen($acsId) > 12);
$this->assertStringContainsString('/saml2/acs?id=', $redirect);
$this->assertTrue(cache()->has('saml2_acs:' . $acsId));
$acsGet = $this->get($redirect);
$acsGet->assertRedirect('/');
$this->assertFalse(cache()->has('saml2_acs:' . $acsId));
$this->assertTrue($this->isAuthenticated());
$this->assertDatabaseHas('users', [
'email' => 'user@example.com',
'external_auth_id' => 'user',
'email_confirmed' => false,
'name' => 'Barry Scott',
]);
}
public function test_acs_process_id_randomly_generated()
{
$acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$redirectA = $acsPost->headers->get('Location');
$acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$redirectB = $acsPost->headers->get('Location');
$this->assertFalse($redirectA === $redirectB);
}
public function test_process_acs_endpoint_cant_be_called_with_invalid_id()
{
$resp = $this->get('/saml2/acs');
$resp->assertRedirect('/login');
$this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
$resp = $this->get('/saml2/acs?id=abc123');
$resp->assertRedirect('/login');
$this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
}
public function test_group_role_sync_on_login()
{
config()->set([
2021-06-26 23:23:15 +08:00
'saml2.onelogin.strict' => false,
'saml2.user_to_groups' => true,
'saml2.remove_from_groups' => false,
]);
$memberRole = Role::factory()->create(['external_auth_id' => 'member']);
$adminRole = Role::getSystemRole('admin');
$this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$user = User::query()->where('external_auth_id', '=', 'user')->first();
$userRoleIds = $user->roles()->pluck('id');
$this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');
$this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');
}
public function test_group_role_sync_removal_option_works_as_expected()
{
config()->set([
2021-06-26 23:23:15 +08:00
'saml2.onelogin.strict' => false,
'saml2.user_to_groups' => true,
'saml2.remove_from_groups' => true,
]);
$acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$user = User::query()->where('external_auth_id', '=', 'user')->first();
$randomRole = Role::factory()->create(['external_auth_id' => 'random']);
$user->attachRole($randomRole);
$this->assertContains($randomRole->id, $user->roles()->pluck('id'));
auth()->logout();
$acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$this->assertNotContains($randomRole->id, $user->roles()->pluck('id'));
}
public function test_logout_link_directs_to_saml_path()
{
config()->set([
'saml2.onelogin.strict' => false,
]);
$resp = $this->actingAs($this->getEditor())->get('/');
$resp->assertElementContains('form[action$="/saml2/logout"] button', 'Logout');
}
public function test_logout_sls_flow()
{
config()->set([
'saml2.onelogin.strict' => false,
]);
$handleLogoutResponse = function () {
$this->assertTrue($this->isAuthenticated());
$req = $this->get('/saml2/sls');
$req->assertRedirect('/');
$this->assertFalse($this->isAuthenticated());
};
$this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$req = $this->post('/saml2/logout');
$redirect = $req->headers->get('location');
$this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);
$this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse);
}
public function test_logout_sls_flow_when_sls_not_configured()
{
config()->set([
2021-06-26 23:23:15 +08:00
'saml2.onelogin.strict' => false,
'saml2.onelogin.idp.singleLogoutService.url' => null,
]);
$this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$this->assertTrue($this->isAuthenticated());
$req = $this->post('/saml2/logout');
$req->assertRedirect('/');
$this->assertFalse($this->isAuthenticated());
}
public function test_dump_user_details_option_works()
{
config()->set([
2021-06-26 23:23:15 +08:00
'saml2.onelogin.strict' => false,
'saml2.dump_user_details' => true,
]);
$acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$acsPost->assertJsonStructure([
'id_from_idp',
'attrs_from_idp' => [],
'attrs_after_parsing' => [],
]);
}
public function test_saml_routes_are_only_active_if_saml_enabled()
{
config()->set(['auth.method' => 'standard']);
$getRoutes = ['/metadata', '/sls'];
foreach ($getRoutes as $route) {
$req = $this->get('/saml2' . $route);
$this->assertPermissionError($req);
}
$postRoutes = ['/login', '/acs', '/logout'];
foreach ($postRoutes as $route) {
$req = $this->post('/saml2' . $route);
$this->assertPermissionError($req);
}
}
public function test_forgot_password_routes_inaccessible()
{
$resp = $this->get('/password/email');
$this->assertPermissionError($resp);
$resp = $this->post('/password/email');
$this->assertPermissionError($resp);
$resp = $this->get('/password/reset/abc123');
$this->assertPermissionError($resp);
$resp = $this->post('/password/reset');
$this->assertPermissionError($resp);
}
public function test_standard_login_routes_inaccessible()
{
$resp = $this->post('/login');
$this->assertPermissionError($resp);
$resp = $this->post('/logout');
$this->assertPermissionError($resp);
}
public function test_user_invite_routes_inaccessible()
{
$resp = $this->get('/register/invite/abc123');
$this->assertPermissionError($resp);
$resp = $this->post('/register/invite/abc123');
$this->assertPermissionError($resp);
}
public function test_user_register_routes_inaccessible()
{
$resp = $this->get('/register');
$this->assertPermissionError($resp);
$resp = $this->post('/register');
$this->assertPermissionError($resp);
}
public function test_email_domain_restriction_active_on_new_saml_login()
{
$this->setSettings([
2021-06-26 23:23:15 +08:00
'registration-restrict' => 'testing.com',
]);
config()->set([
'saml2.onelogin.strict' => false,
]);
$acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$acsPost->assertSeeText('That email domain does not have access to this application');
$this->assertFalse(auth()->check());
$this->assertDatabaseMissing('users', ['email' => 'user@example.com']);
}
public function test_group_sync_functions_when_email_confirmation_required()
{
setting()->put('registration-confirmation', 'true');
config()->set([
2021-06-26 23:23:15 +08:00
'saml2.onelogin.strict' => false,
'saml2.user_to_groups' => true,
'saml2.remove_from_groups' => false,
]);
$memberRole = Role::factory()->create(['external_auth_id' => 'member']);
$adminRole = Role::getSystemRole('admin');
$acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$this->assertEquals('http://localhost/register/confirm', url()->current());
$acsPost->assertSee('Please check your email and click the confirmation button to access BookStack.');
/** @var User $user */
$user = User::query()->where('external_auth_id', '=', 'user')->first();
$userRoleIds = $user->roles()->pluck('id');
$this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');
$this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');
$this->assertFalse(boolval($user->email_confirmed), 'User email remains unconfirmed');
$this->assertNull(auth()->user());
$homeGet = $this->get('/');
$homeGet->assertRedirect('/login');
}
public function test_login_where_existing_non_saml_user_shows_warning()
{
$this->post('/saml2/login');
config()->set(['saml2.onelogin.strict' => false]);
// Make the user pre-existing in DB with different auth_id
User::query()->forceCreate([
2021-06-26 23:23:15 +08:00
'email' => 'user@example.com',
'external_auth_id' => 'old_system_user_id',
2021-06-26 23:23:15 +08:00
'email_confirmed' => false,
'name' => 'Barry Scott',
]);
$acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
$this->assertFalse($this->isAuthenticated());
$this->assertDatabaseHas('users', [
'email' => 'user@example.com',
'external_auth_id' => 'old_system_user_id',
]);
$acsPost->assertSee('A user with the email user@example.com already exists but with different credentials');
}
public function test_login_request_contains_expected_default_authncontext()
{
$authReq = $this->getAuthnRequest();
$this->assertStringContainsString('samlp:RequestedAuthnContext Comparison="exact"', $authReq);
$this->assertStringContainsString('<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>', $authReq);
}
public function test_false_idp_authncontext_option_does_not_pass_authncontext_in_saml_request()
{
config()->set(['saml2.onelogin.security.requestedAuthnContext' => false]);
$authReq = $this->getAuthnRequest();
$this->assertStringNotContainsString('samlp:RequestedAuthnContext', $authReq);
$this->assertStringNotContainsString('<saml:AuthnContextClassRef>', $authReq);
}
public function test_array_idp_authncontext_option_passes_value_as_authncontextclassref_in_request()
{
config()->set(['saml2.onelogin.security.requestedAuthnContext' => ['urn:federation:authentication:windows', 'urn:federation:authentication:linux']]);
$authReq = $this->getAuthnRequest();
$this->assertStringContainsString('samlp:RequestedAuthnContext', $authReq);
$this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:windows</saml:AuthnContextClassRef>', $authReq);
$this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:linux</saml:AuthnContextClassRef>', $authReq);
}
protected function getAuthnRequest(): string
{
$req = $this->post('/saml2/login');
$location = $req->headers->get('Location');
$query = explode('?', $location)[1];
$params = [];
parse_str($query, $params);
2021-06-26 23:23:15 +08:00
return gzinflate(base64_decode($params['SAMLRequest']));
}
protected function withGet(array $options, callable $callback)
{
return $this->withGlobal($_GET, $options, $callback);
}
protected function withGlobal(array &$global, array $options, callable $callback)
{
$original = [];
foreach ($options as $key => $val) {
$original[$key] = $global[$key] ?? null;
$global[$key] = $val;
}
$callback();
foreach ($options as $key => $val) {
$val = $original[$key];
if ($val) {
$global[$key] = $val;
} else {
unset($global[$key]);
}
}
}
/**
* The post data for a callback for single-sign-in.
* Provides the following attributes:
* array:5 [
2021-06-26 23:23:15 +08:00
* "uid" => array:1 [
* 0 => "user"
* ]
* "first_name" => array:1 [
* 0 => "Barry"
* ]
* "last_name" => array:1 [
* 0 => "Scott"
* ]
* "email" => array:1 [
* 0 => "user@example.com"
* ]
* "user_groups" => array:2 [
* 0 => "member"
* 1 => "admin"
* ]
* ].
*/
protected $acsPostData = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxOS0xMS0xN1QxNzo1MzozOVoiIERlc3RpbmF0aW9uPSJodHRwOi8vYm9va3N0YWNrLmxvY2FsL3NhbWwyL2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82YTBmNGYzOTkzMDQwZjE5ODdmZDM3MDY4YjUyOTYyMjlhZDUzNjFjIj48c2FtbDpJc3N1ZXI+aHR0cDovL3NhbWwubG9jYWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+dm1oL1M3NU5mK2crZWNESkN6QWJaV0tKVmx1ZzdCZnNDKzlhV05lSXJlUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dnJhZ0tKWHNjVm5UNjJFaEk3bGk4MERUWHNOTGJOc3lwNWZ2QnU4WjFYSEtFUVA3QWpPNkcxcVBwaGpWQ2dRMzd6TldVVTZvUytQeFA3UDlHeG5xL3hKejRUT3lHcHJ5N1RoK2pIcHc0YWVzQTdrTmp6VU51UmU2c1ltWTlrRXh2VjMvTmJRZjROMlM2Y2RhRHIzWFRodllVVDcxYzQwNVVHOFJpQjJaY3liWHIxZU1yWCtXUDBnU2Qrc0F2RExqTjBJc3pVWlVUNThadFpEVE1ya1ZGL0pIbFBFQ04vVW1sYVBBeitTcUJ4c25xTndZK1oxYUt3MnlqeFRlNnUxM09Kb29OOVN1REowNE0rK2F3RlY3NkI4cXEyTzMxa3FBbDJibm1wTGxtTWdRNFEraUlnL3dCc09abTV1clphOWJObDNLVEhtTVBXbFpkbWhsLzgvMy9IT1RxN2thWGs3cnlWRHRLcFlsZ3FUajNhRUpuL0dwM2o4SFp5MUVialRiOTRRT1ZQMG5IQzB1V2hCaE13TjdzVjFrUSsxU2NjUlpUZXJKSGlSVUQvR0srTVg3M0YrbzJVTFRIL1Z6Tm9SM2o4N2hOLzZ1UC9JeG5aM1RudGR1MFZPZS9ucEdVWjBSMG9SWFhwa2JTL2poNWk1ZjU0RXN4eXZ1VEM5NHdKaEM8L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlFYXpDQ0F0T2dBd0lCQWdJVWU3YTA4OENucjRpem1ybkJFbng1cTNIVE12WXdEUVlKS29aSWh2Y05BUUVMQlFBd1JURUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTUdFbHVkR1Z5Ym1WMElGZHBaR2RwZEhNZ1VIUjVJRXgwWkRBZUZ3MHhPVEV4TVRZeE1qRTNNVFZhRncweU9URXhNVFV4TWpFM01UVmFNRVV4Q3pBSkJnTlZCQVlUQWtkQ01STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FEekxlOUZmZHlwbFR4SHA0U3VROWdRdFpUM3QrU0RmdkVMNzJwcENmRlp3NytCNXM1Qi9UNzNhWHBvUTNTNTNwR0kxUklXQ2dlMmlDVVEydHptMjdhU05IMGl1OWFKWWNVUVovUklUcWQwYXl5RGtzMU5BMlBUM1RXNnQzbTdLVjVyZTRQME5iK1lEZXV5SGRreitqY010cG44Q21Cb1QwSCtza2hhMGhpcUlOa2prUlBpSHZMSFZHcCt0SFVFQS9JNm1ONGFCL1VFeFNUTHM3OU5zTFVmdGVxcXhlOSt0dmRVYVRveURQcmhQRmpPTnMrOU5LQ2t6SUM2dmN2N0o2QXR1S0c2bkVUK3pCOXlPV2d0R1lRaWZYcVFBMnk1ZEw4MUJCMHE1dU1hQkxTMnBxM2FQUGp6VTJGMytFeXNqeVNXVG5Da2ZrN0M1U3NDWFJ1OFErVTk1dHVucE5md2Y1b2xFNldhczQ4Tk1NK1B3VjdpQ05NUGtOemxscTZQQ2lNK1A4RHJNU2N6elVaWlFVU3Y2ZFN3UENvK1lTVmltRU0wT2czWEpUaU5oUTVBTmxhSW42Nkt3NWdmb0JmdWlYbXlJS2lTRHlBaURZbUZhZjQzOTV3V3dMa1RSK2N3OFdmamFIc3dLWlRvbW4xTVIzT0pzWTJVSjBlUkJZTStZU3NDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01COEdBMVVkSXdRWU1CYUFGSW1wMkNZQ0dmY2I3dzkxSC9jU2hUQ2tYd1IvTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0dCQUErZy9DN3VMOWxuK1crcUJrbkxXODFrb2pZZmxnUEsxSTFNSEl3bk12bC9aVEhYNGRSWEtEcms3S2NVcTFLanFhak5WNjZmMWNha3AwM0lpakJpTzBYaTFnWFVaWUxvQ2lOR1V5eXA5WGxvaUl5OVh3MlBpV25ydzAreVp5dlZzc2JlaFhYWUpsNFJpaEJqQld1bDlSNHdNWUxPVVNKRGUyV3hjVUJoSm54eU5ScytQMHhMU1FYNkIybjZueG9Ea280cDA3czhaS1hRa2VpWjJpd0ZkVHh6UmtHanRoTVV2NzA0bnpzVkdCVDBEQ1B0ZlNhTzVLSlpXMXJDczN5aU10aG5CeHE0cUVET1FKRklsKy9MRDcxS2JCOXZaY1c1SnVhdnpCRm1rS0dOcm8vNkcxSTdlbDQ2SVI0d2lqVHlORkNZVXVEOWR0aWduTm1wV3ROOE9XK3B0aUwvanRUeVNXdWtqeXMwcyt2TG44M0NWdmpCMGRKdFZBSVlPZ1hGZ
protected $sloResponseData = 'fZHRa8IwEMb/lZJ3bdJa04a2MOYYglOY4sNe5JKms9gmpZfC/vxF3ZjC8OXgLvl938ddjtC1vVjZTzu6d429NaiDr641KC5PBRkHIyxgg8JAp1E4JbZPbysRTanoB+ussi25QR4TgKgH11hDguWiIIeawTxOaK1iPYt5XcczHUlJeVRlMklBJjOuM1qDVCTY6wE9WRAv5HHEUS8NOjDOjyjLJoxNGN+xVESpSNgHCRYaXWPAXaijc70IQ2ntyUPqNG2tgjY8Z45CbNFLmt8V7GxBNuuX1eZ1uT7EcZJKAE4TJhXPaMxlVlFffPKKJnXE5ryusoiU+VlMXJIN5Y/feXRn1VR92GkHFTiY9sc+D2+p/HqRrQM34n33bCsd7KEd9eMd4+W32I5KaUQSlleHP9Hwv6uX3w==';
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==';
}