]> BookStack Code Mirror - bookstack/blobdiff - tests/Auth/Saml2Test.php
add tests for priority
[bookstack] / tests / Auth / Saml2Test.php
index 8ace3e2ee4f19dd9ea8fee11f606f2225f601277..801682a003c2a80722efc33d39485a34ca925b23 100644 (file)
@@ -2,13 +2,13 @@
 
 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
@@ -41,6 +41,20 @@ class Saml2Test extends TestCase
         $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]"}}}';
@@ -49,14 +63,14 @@ class Saml2Test extends TestCase
         $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()
@@ -68,17 +82,47 @@ class Saml2Test extends TestCase
         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', [
-                'email'            => '[email protected]',
-                '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', [
+            'email'            => '[email protected]',
+            '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()
@@ -89,17 +133,15 @@ class Saml2Test extends TestCase
             '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()
@@ -110,18 +152,16 @@ class Saml2Test extends TestCase
             '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()
@@ -130,9 +170,8 @@ class Saml2Test extends TestCase
             '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()
@@ -149,16 +188,15 @@ class Saml2Test extends TestCase
             $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()
@@ -168,15 +206,12 @@ class Saml2Test extends TestCase
             '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()
@@ -186,26 +221,24 @@ class Saml2Test extends TestCase
             '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);
@@ -232,7 +265,7 @@ class Saml2Test extends TestCase
         $resp = $this->post('/login');
         $this->assertPermissionError($resp);
 
-        $resp = $this->get('/logout');
+        $resp = $this->post('/logout');
         $this->assertPermissionError($resp);
     }
 
@@ -263,13 +296,10 @@ class Saml2Test extends TestCase
             '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);
-            $this->assertDatabaseMissing('users', ['email' => '[email protected]']);
-        });
+        $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' => '[email protected]']);
     }
 
     public function test_group_sync_functions_when_email_confirmation_required()
@@ -281,22 +311,20 @@ class Saml2Test extends TestCase
             '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('/');
@@ -316,18 +344,14 @@ class Saml2Test extends TestCase
             'name'             => 'Barry Scott',
         ]);
 
-        $this->withPost(['SAMLResponse' => $this->acsPostData], function () {
-            $acsPost = $this->post('/saml2/acs');
-            $acsPost->assertRedirect('/login');
-            $this->assertFalse($this->isAuthenticated());
-            $this->assertDatabaseHas('users', [
-                'email'            => '[email protected]',
-                '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', [
+            'email'            => '[email protected]',
+            '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()
@@ -358,11 +382,16 @@ class Saml2Test extends TestCase
     {
         $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)
@@ -370,11 +399,6 @@ class Saml2Test extends TestCase
         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 = [];