From 1877042c999e2e12be3f4f72b55bffec6bd9b3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=BB=C3=B3=C5=82tak?= Date: Wed, 16 Oct 2024 15:15:04 +0200 Subject: [PATCH] /user/logout endpoint implemented It provides best possible but still ugly from the user perspective logout option for the HTTP Basic authentication. Later on this should be moved to the auth library. --- src/acdhOeaw/arche/core/Auth.php | 11 +++++++++ src/acdhOeaw/arche/core/UserApi.php | 30 ++++++++++++----------- tests/UserApiTest.php | 38 ++++++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/acdhOeaw/arche/core/Auth.php b/src/acdhOeaw/arche/core/Auth.php index c8834f3..2046090 100644 --- a/src/acdhOeaw/arche/core/Auth.php +++ b/src/acdhOeaw/arche/core/Auth.php @@ -218,4 +218,15 @@ public function denyAccess(array $allowed): void { } throw new RepoException('Forbidden', 403); } + + public function logout(string $redirectUrl = ''): void { + // the right way would be to have a $this->controller->logout() + unset($_SERVER['PHP_AUTH_USER'], $_SERVER['HTTP_AUTHORIZATION'], $_SERVER['AUTHORIZATION']); + $this->controller->advertise(); + + if (!empty($redirectUrl)) { + header("Refresh: 0; url=$redirectUrl"); + } + throw new RepoException('Logged out', 401); + } } diff --git a/src/acdhOeaw/arche/core/UserApi.php b/src/acdhOeaw/arche/core/UserApi.php index 4f456a9..54cf715 100644 --- a/src/acdhOeaw/arche/core/UserApi.php +++ b/src/acdhOeaw/arche/core/UserApi.php @@ -67,10 +67,21 @@ public function put(string $user): void { } public function get(string $user): void { - if (RC::$auth->isPublic() || !RC::$auth->isAdmin() && !empty($user) && $user !== RC::$auth->getUserName()) { + if (RC::$auth->isPublic() || !RC::$auth->isAdmin() && !empty($user) && $user !== RC::$auth->getUserName() && $user !== 'logout') { RC::$auth->denyAccess([$user]); } - + + $redirect = $_GET['redirect'] ?? ''; + $redirectRegex = RC::$config->rest->userEndpointAllowedRedirectRegex ?? '/^$/'; + if (!empty($redirect)) { + if (preg_match($redirectRegex, $redirect)) { + RC::setHeader('Location', $redirect); + http_response_code(303); + } else { + throw new RepoException('Redirect location not allowed', 400); + } + } + if (empty($user)) { $where = ''; $param = []; @@ -86,20 +97,11 @@ public function get(string $user): void { foreach ($users as $user) { $data[] = $this->prepareUserData($this->db->getUser($user), $user); } - } else { + } elseif ($user !== 'logout') { $data = $this->checkUserExists($user); $data = $this->prepareUserData($data, $user); - } - - $redirect = $_GET['redirect'] ?? ''; - $redirectRegex = RC::$config->rest->userEndpointAllowedRedirectRegex ?? '/^$/'; - if (!empty($redirect)) { - if (preg_match($redirectRegex, $redirect)) { - RC::setHeader('Location', $redirect); - http_response_code(303); - } else { - throw new RepoException('Redirect location not allowed', 400); - } + } else { + RC::$auth->logout($redirect); } $data = json_encode($data) ?: throw new \RuntimeException("Can't serialise to JSON"); diff --git a/tests/UserApiTest.php b/tests/UserApiTest.php index 4a998b5..d69645e 100644 --- a/tests/UserApiTest.php +++ b/tests/UserApiTest.php @@ -183,10 +183,10 @@ public function testUserGet(): void { $this->assertEquals(['archeLogin=bar%2CpublicRole%2Cacademic; path=/'], $resp->getHeader('Set-Cookie')); // /user by a non-admin user - $req = new Request('get', self::$baseUrl . 'user', $headers); - $resp = self::$client->send($req); + $req = new Request('get', self::$baseUrl . 'user', $headers); + $resp = self::$client->send($req); $this->assertEquals(200, $resp->getStatusCode()); - $data = json_decode($resp->getBody()); + $data = json_decode($resp->getBody()); $this->assertIsArray($data); $this->assertCount(1, $data); $data = $data[0]; @@ -196,7 +196,7 @@ public function testUserGet(): void { $this->assertFalse(isset($data->password)); $this->assertFalse(isset($data->pswd)); $this->assertEquals(['archeLogin=bar%2CpublicRole%2Cacademic; path=/'], $resp->getHeader('Set-Cookie')); - + // lack of priviledges $headers = ['Authorization' => 'Basic ' . base64_encode('foo:' . self::PSWD)]; $req = new Request('get', self::$baseUrl . 'user/bar', $headers); @@ -250,6 +250,36 @@ public function testRedirect(): void { $this->assertEquals(['archeLogin=bar%2CpublicRole%2Cacademic; path=/'], $resp->getHeader('Set-Cookie')); } + #[Depends('testUserCreate')] + public function testUserLogout(): void { + $headers = ['Authorization' => 'Basic ' . base64_encode('foo:' . self::PSWD)]; + + // logout without credentials + $req = new Request('get', self::$baseUrl . 'user/logout?redirect=/foo'); + $resp = self::$client->send($req); + $this->assertEquals(401, $resp->getStatusCode()); + $this->assertEquals([], $resp->getHeader('Refresh')); + + // logout with invalid credentials + $req = new Request('get', self::$baseUrl . 'user/logout?redirect=/foo', [ + 'Authorization' => 'Basic ' . base64_encode('x:y')]); + $resp = self::$client->send($req); + $this->assertEquals(403, $resp->getStatusCode()); + $this->assertEquals([], $resp->getHeader('Refresh')); + + // logout with valid credentials + $req = new Request('get', self::$baseUrl . 'user/logout', $headers); + $resp = self::$client->send($req); + $this->assertEquals(401, $resp->getStatusCode()); + $this->assertEquals([], $resp->getHeader('Refresh')); + + // logout with redirect + $req = new Request('get', self::$baseUrl . 'user/logout?redirect=' . rawurldecode('/foo/bar'), $headers); + $resp = self::$client->send($req); + $this->assertEquals(401, $resp->getStatusCode()); + $this->assertEquals(['0; url=/foo/bar'], $resp->getHeader('Refresh')); + } + #[Depends('testUserCreate')] public function testUserPatch(): void { // as root