namespace Tests\Auth;
-use BookStack\Auth\Role;
-use BookStack\Auth\User;
+use BookStack\Users\Models\Role;
+use BookStack\Users\Models\User;
use Tests\TestCase;
class Saml2Test extends TestCase
{
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
// Set default config for SAML2
$req->assertSee(url('/saml2/acs'));
}
+ public function test_metadata_endpoint_loads_when_autoloading_with_bad_url_set()
+ {
+ config()->set([
+ 'saml2.autoload_from_metadata' => true,
+ 'saml2.onelogin.idp.entityId' => 'http://192.168.1.1:9292',
+ 'saml2.onelogin.idp.singleSignOnService.url' => null,
+ ]);
+
+ $req = $this->get('/saml2/metadata');
+ $req->assertOk();
+ $req->assertHeader('Content-Type', 'text/xml; charset=UTF-8');
+ $req->assertSee('md:EntityDescriptor');
+ }
+
public function test_onelogin_overrides_functions_as_expected()
{
$json = '{"sp": {"assertionConsumerService": {"url": "https://example.com/super-cats"}}, "contactPerson": {"technical": {"givenName": "Barry Scott", "emailAddress": "
[email protected]"}}}';
$req = $this->get('/saml2/metadata');
$req->assertSee('https://example.com/super-cats');
$req->assertSee('md:ContactPerson');
- $req->assertSee('<md:GivenName>Barry Scott</md:GivenName>');
+ $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');
+ $this->withHtml($req)->assertElementExists('form[action$="/saml2/login"][method=POST] button');
}
public function test_login()
config()->set(['saml2.onelogin.strict' => false]);
$this->assertFalse($this->isAuthenticated());
- $this->withPost(['SAMLResponse' => $this->acsPostData], function () {
- $acsPost = $this->post('/saml2/acs');
- $acsPost->assertRedirect('/');
- $this->assertTrue($this->isAuthenticated());
- $this->assertDatabaseHas('users', [
- 'external_auth_id' => 'user',
- 'email_confirmed' => false,
- 'name' => 'Barry Scott',
- ]);
- });
+ $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', [
+ '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()
'saml2.remove_from_groups' => false,
]);
- $memberRole = factory(Role::class)->create(['external_auth_id' => 'member']);
+ $memberRole = Role::factory()->create(['external_auth_id' => 'member']);
$adminRole = Role::getSystemRole('admin');
- $this->withPost(['SAMLResponse' => $this->acsPostData], function () use ($memberRole, $adminRole) {
- $acsPost = $this->post('/saml2/acs');
- $user = User::query()->where('external_auth_id', '=', 'user')->first();
+ $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');
- });
+ $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()
'saml2.remove_from_groups' => true,
]);
- $this->withPost(['SAMLResponse' => $this->acsPostData], function () {
- $acsPost = $this->post('/saml2/acs');
- $user = User::query()->where('external_auth_id', '=', 'user')->first();
+ $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
+ $user = User::query()->where('external_auth_id', '=', 'user')->first();
- $randomRole = factory(Role::class)->create(['external_auth_id' => 'random']);
- $user->attachRole($randomRole);
- $this->assertContains($randomRole->id, $user->roles()->pluck('id'));
+ $randomRole = Role::factory()->create(['external_auth_id' => 'random']);
+ $user->attachRole($randomRole);
+ $this->assertContains($randomRole->id, $user->roles()->pluck('id'));
- auth()->logout();
- $acsPost = $this->post('/saml2/acs');
- $this->assertNotContains($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()
'saml2.onelogin.strict' => false,
]);
- $resp = $this->actingAs($this->getEditor())->get('/');
- $resp->assertElementExists('a[href$="/saml2/logout"]');
- $resp->assertElementContains('a[href$="/saml2/logout"]', 'Logout');
+ $resp = $this->actingAs($this->users->editor())->get('/');
+ $this->withHtml($resp)->assertElementContains('form[action$="/saml2/logout"] button', 'Logout');
}
public function test_logout_sls_flow()
$this->assertFalse($this->isAuthenticated());
};
- $loginAndStartLogout = function () use ($handleLogoutResponse) {
- $this->post('/saml2/acs');
+ $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
- $req = $this->get('/saml2/logout');
- $redirect = $req->headers->get('location');
- $this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);
- $this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse);
- };
+ $req = $this->post('/saml2/logout');
+ $redirect = $req->headers->get('location');
+ $this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);
+ $sloData = $this->parseSamlDataFromUrl($redirect, 'SAMLRequest');
+ $this->assertStringContainsString('<samlp:SessionIndex>_4fe7c0d1572d64b27f930aa6f236a6f42e930901cc</samlp:SessionIndex>', $sloData);
- $this->withPost(['SAMLResponse' => $this->acsPostData], $loginAndStartLogout);
+ $this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse);
}
public function test_logout_sls_flow_when_sls_not_configured()
'saml2.onelogin.idp.singleLogoutService.url' => null,
]);
- $loginAndStartLogout = function () {
- $this->post('/saml2/acs');
-
- $req = $this->get('/saml2/logout');
- $req->assertRedirect('/');
- $this->assertFalse($this->isAuthenticated());
- };
+ $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
+ $this->assertTrue($this->isAuthenticated());
- $this->withPost(['SAMLResponse' => $this->acsPostData], $loginAndStartLogout);
+ $req = $this->post('/saml2/logout');
+ $req->assertRedirect('/');
+ $this->assertFalse($this->isAuthenticated());
}
public function test_dump_user_details_option_works()
'saml2.dump_user_details' => true,
]);
- $this->withPost(['SAMLResponse' => $this->acsPostData], function () {
- $acsPost = $this->post('/saml2/acs');
- $acsPost->assertJsonStructure([
- 'id_from_idp',
- 'attrs_from_idp' => [],
- 'attrs_after_parsing' => [],
- ]);
- });
+ $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 = ['/logout', '/metadata', '/sls'];
+ $getRoutes = ['/metadata', '/sls'];
foreach ($getRoutes as $route) {
$req = $this->get('/saml2' . $route);
$this->assertPermissionError($req);
}
- $postRoutes = ['/login', '/acs'];
+ $postRoutes = ['/login', '/acs', '/logout'];
foreach ($postRoutes as $route) {
$req = $this->post('/saml2' . $route);
$this->assertPermissionError($req);
$resp = $this->post('/login');
$this->assertPermissionError($resp);
- $resp = $this->get('/logout');
+ $resp = $this->post('/logout');
$this->assertPermissionError($resp);
}
'saml2.onelogin.strict' => false,
]);
- $this->withPost(['SAMLResponse' => $this->acsPostData], function () {
- $acsPost = $this->post('/saml2/acs');
- $acsPost->assertRedirect('/login');
- $errorMessage = session()->get('error');
- $this->assertStringContainsString('That email domain does not have access to this application', $errorMessage);
- });
+ $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());
}
public function test_group_sync_functions_when_email_confirmation_required()
'saml2.remove_from_groups' => false,
]);
- $memberRole = factory(Role::class)->create(['external_auth_id' => 'member']);
+ $memberRole = Role::factory()->create(['external_auth_id' => 'member']);
$adminRole = Role::getSystemRole('admin');
- $this->withPost(['SAMLResponse' => $this->acsPostData], function () use ($memberRole, $adminRole) {
- $acsPost = $this->followingRedirects()->post('/saml2/acs');
+ $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();
+ $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');
- });
+ $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('/');
'name' => 'Barry Scott',
]);
- $this->withPost(['SAMLResponse' => $this->acsPostData], function () {
- $acsPost = $this->post('/saml2/acs');
- $acsPost->assertRedirect('/login');
- $this->assertFalse($this->isAuthenticated());
- $this->assertDatabaseHas('users', [
- 'external_auth_id' => 'old_system_user_id',
- ]);
-
- $loginGet = $this->get('/login');
- $loginGet->assertSee('A user with the email
[email protected] already exists but with different credentials');
- });
+ $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]);
+ $this->assertFalse($this->isAuthenticated());
+ $this->assertDatabaseHas('users', [
+ 'external_auth_id' => 'old_system_user_id',
+ ]);
+
+ $acsPost->assertSee('A user with the email
[email protected] already exists but with different credentials');
}
public function test_login_request_contains_expected_default_authncontext()
{
$req = $this->post('/saml2/login');
$location = $req->headers->get('Location');
- $query = explode('?', $location)[1];
+ return $this->parseSamlDataFromUrl($location, 'SAMLRequest');
+ }
+
+ protected function parseSamlDataFromUrl(string $url, string $paramName): string
+ {
+ $query = explode('?', $url)[1];
$params = [];
parse_str($query, $params);
- return gzinflate(base64_decode($params['SAMLRequest']));
+ return gzinflate(base64_decode($params[$paramName]));
}
protected function withGet(array $options, callable $callback)
return $this->withGlobal($_GET, $options, $callback);
}
- protected function withPost(array $options, callable $callback)
- {
- return $this->withGlobal($_POST, $options, $callback);
- }
-
protected function withGlobal(array &$global, array $options, callable $callback)
{
$original = [];