diff --git a/lib/REST/Microsub/Auth.php b/lib/REST/Microsub/Auth.php index 29f6f65..70abd4d 100644 --- a/lib/REST/Microsub/Auth.php +++ b/lib/REST/Microsub/Auth.php @@ -310,7 +310,9 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { return [$token['user'], $data['response_type'] ?? "id"]; } - /** Handles token verification as an API call + /** + * Handles token verification as an API call; this will not normally be used since + * the token and service endpoints are tightly coupled * * The static `validateBearer` method should be used to check the validity of a bearer token in normal use * @@ -321,23 +323,27 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { if (!$req->hasHeader("Authorization")) { throw new ExceptionAuth("invalid_token"); } - $authorization = $req->getHeader("Authorization")[0]; - list($user, $data) = self::validateBearer($authorization); + $authorization = $req->getHeader("Authorization"); + if (sizeof($authorization) > 1) { + throw new ExceptionAuth("invalid_request"); + } + list($user, $data) = self::validateBearer($authorization[0]); } catch (ExceptionAuth $e) { $errCode = $e->getMessage(); $httpCode = [ 'invalid_request' => 400, 'invalid_token' => 401, ][$errCode] ?? 500; - return new EmptyResponse($httpCode, [ - 'WWW-Authenticate' => "Bearer error=\"$errCode\"", - 'X-Arsse-Suppress-General-Auth' => "1" - ]); + $out = new EmptyResponse($httpCode, ['WWW-Authenticate' => "Bearer error=\"$errCode\""]); + if ($httpCode == 401) { + $out = $out->withHeader("X-Arsse-Suppress-General-Auth", "1"); + } + return $out; } return new JsonResponse([ 'me' => $data['me'] ?? "", 'client_id' => $data['client_id'] ?? "", - 'scope' => implode(" ", ($data['scope'] ?? self::SCOPES)), + 'scope' => implode(" ", (array) ($data['scope'] ?? self::SCOPES)), ]); } @@ -352,7 +358,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { } try { $info = Arsse::$db->tokenLookup("microsub.access", $token); - Arsse::$db->tokenRevoke($info['user'], "mucrosub.access", $token); + Arsse::$db->tokenRevoke($info['user'], "microsub.access", $token); } catch (\JKingWeb\Arsse\Db\ExceptionInput $e) { } return new EmptyResponse(200); diff --git a/tests/cases/REST/Microsub/TestAuth.php b/tests/cases/REST/Microsub/TestAuth.php index a12d713..c04e4e8 100644 --- a/tests/cases/REST/Microsub/TestAuth.php +++ b/tests/cases/REST/Microsub/TestAuth.php @@ -249,4 +249,59 @@ class TestAuth extends \JKingWeb\Arsse\Test\AbstractTest { 'Broken data' => ["Bearer TOKEN", [], "TOKEN", "someone", '{', ["someone", ['scope' => Auth::SCOPES]]], ]; } + + /** @dataProvider provideRevocations */ + public function testRevokeAToken(array $params, $user, ResponseInterface $exp) { + \Phake::when(Arsse::$db)->tokenRevoke->thenReturn(true); + if ($user instanceof \Exception) { + \Phake::when(Arsse::$db)->tokenLookup->thenThrow($user); + } else { + \Phake::when(Arsse::$db)->tokenLookup->thenReturn(['user' => $user]); + } + $this->assertMessage($exp, $this->req("http://example.com/u/?f=token", "POST", [], [], array_merge(['action' => "revoke"], $params))); + $doLookup = strlen($params['token'] ?? "") > 0; + $doRevoke = ($doLookup && !$user instanceof \Exception); + if ($doLookup) { + \Phake::verify(Arsse::$db)->tokenLookup("microsub.access", $params['token'] ?? ""); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->tokenLookup; + } + if ($doRevoke) { + \Phake::verify(Arsse::$db)->tokenRevoke($user, "microsub.access", $params['token'] ?? ""); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->tokenRevoke; + } + } + + public function provideRevocations() { + return [ + 'Missing token 1' => [[], "", new EmptyResponse(422)], + 'Missing token 2' => [['token' => ""], "", new EmptyResponse(422)], + 'Bad Token' => [['token' => "bad"], new ExceptionInput("subjectMissing"), new EmptyResponse(200)], + 'Success' => [['token' => "good"], "someone", new EmptyResponse(200)], + ]; + } + + /** @dataProvider provideTokenVerifications */ + public function testVerifyAToken(array $authorization, $output, ResponseInterface $exp) { + if ($output instanceof \Exception) { + \Phake::when(Arsse::$db)->tokenLookup->thenThrow($output); + } else { + \Phake::when(Arsse::$db)->tokenLookup->thenReturn(['user' => "someone", 'data' => $output]); + } + $this->assertMessage($exp, $this->req("http://example.com/u/?f=token", "GET", [], $authorization ? ['Authorization' => $authorization] : [])); + \Phake::verify(Arsse::$db, \Phake::times(0))->tokenRevoke; + } + + public function provideTokenVerifications() { + return [ + 'No credentials' => [[], "", new EmptyResponse(401, ['WWW-Authenticate' => 'Bearer error="invalid_token"', 'X-Arsse-Suppress-General-Auth' => "1"])], + 'Too many credentials' => [["Bearer TOKEN", "Basic BASE64"], "", new EmptyResponse(400, ['WWW-Authenticate' => 'Bearer error="invalid_request"'])], + 'Invalid credentials' => [["Bearer !"], "", new EmptyResponse(400, ['WWW-Authenticate' => 'Bearer error="invalid_request"'])], + 'Bad credentials' => [["Bearer BAD"], new ExceptionInput("subjectMissing"), new EmptyResponse(401, ['WWW-Authenticate' => 'Bearer error="invalid_token"', 'X-Arsse-Suppress-General-Auth' => "1"])], + 'Success 1' => [["Bearer GOOD"], '{"me":"ook","client_id":"eek","scope":["ack"]}', new Response(['me' => "ook", 'client_id' => "eek", 'scope' => "ack"])], + 'Success 2' => [["Bearer GOOD"], '{"scope":["ook","eek","ack"]}', new Response(['me' => "", 'client_id' => "", 'scope' => "ook eek ack"])], + 'Success 3' => [["Bearer GOOD"], '{}', new Response(['me' => "", 'client_id' => "", 'scope' => "read follow channels"])], + ]; + } }