diff --git a/lib/REST/Microsub/Auth.php b/lib/REST/Microsub/Auth.php index e3bf51f..ed2ecfa 100644 --- a/lib/REST/Microsub/Auth.php +++ b/lib/REST/Microsub/Auth.php @@ -125,7 +125,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { $me['scheme'] = $me['scheme'] ?? ""; $me['path'] = explode("/", $me['path'] ?? ""); $me['id'] = rawurldecode(array_pop($me['path']) ?? ""); - $me['port'] == (($me['scheme'] === "http" && $me['port'] == 80) || ($me['scheme'] === "https" && $me['port'] == 443)) ? 0 : $me['port']; + $me['port'] = (($me['scheme'] === "http" && ($me['port'] ?? 80) == 80) || ($me['scheme'] === "https" && ($me['port'] ?? 443) == 443)) ? 0 : $me['port'] ?? 0; $c = parse_url($canonical); $c['path'] = explode("/", $c['path']); $c['id'] = rawurldecode(array_pop($c['path'])); @@ -187,12 +187,13 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { } else { // user has logged in $query = $req->getQueryParams(); - $redir = URL::normalize(rawurldecode($query['redirect_uri'])); + $redir = URL::normalize($query['redirect_uri']); // check that the redirect URL is an absolute one if (!URL::absolute($redir)) { return new EmptyResponse(400); } try { + $state = $query['state'] ?? ""; // ensure the logged-in user matches the IndieAuth identifier URL $user = $req->getAttribute("authenticatedUser"); if (!$this->matchIdentifier($this->buildIdentifier($req, $user), $query['me'])) { @@ -202,21 +203,20 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { if (!in_array($type, ["code", "id"])) { throw new ExceptionAuth("unsupported_response_type"); } - $state = $query['state'] ?? ""; // store the identity URL, client ID, redirect URL, and response type $data = json_encode([ 'me' => $query['me'], - 'client' => $query['client_id'], - 'redir' => $query['redirect_uri'], - 'type' => $type, + 'client_id' => $query['client_id'], + 'redirect_uri' => $query['redirect_uri'], + 'response_type' => $type, ], \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); // issue an authorization code and build the redirect URL $code = Arsse::$db->tokenCreate($user, "microsub.auth", null, Date::add("PT2M"), $data); $next = URL::queryAppend($redir, "code=$code&state=$state"); - return new EmptyResponse(302, ["Location: $next"]); + return new EmptyResponse(302, ['Location' => $next]); } catch (ExceptionAuth $e) { $next = URL::queryAppend($redir, "state=$state&error=".$e->getMessage()); - return new EmptyResponse(302, ["Location: $next"]); + return new EmptyResponse(302, ['Location' => $next]); } } } @@ -297,13 +297,13 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { // validate the auth code if (!is_array($data)) { throw new ExceptionAuth("invalid_grant"); - } elseif ($data['client'] !== $clientId || $data['redir'] !== $redirUrl) { + } elseif ($data['client_id'] !== $clientId || $data['redirect_uri'] !== $redirUrl) { throw new ExceptionAuth("invalid_client"); } elseif (isset($me) && $me !== $data['me']) { throw new ExceptionAuth("invalid_grant"); } // return the associated user name and the auth-code type - return [$token['user'], $data['type'] ?? "id"]; + return [$token['user'], $data['response_type'] ?? "id"]; } /** Handles token verification as an API call diff --git a/tests/cases/REST/Microsub/TestAuth.php b/tests/cases/REST/Microsub/TestAuth.php index 595599a..7617fcb 100644 --- a/tests/cases/REST/Microsub/TestAuth.php +++ b/tests/cases/REST/Microsub/TestAuth.php @@ -6,6 +6,8 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\REST\Microsub; +use JKingWeb\Arsse\Arsse; +use JKingWeb\Arsse\Database; use Psr\Http\Message\ResponseInterface; use Zend\Diactoros\Response\JsonResponse as Response; use Zend\Diactoros\Response\EmptyResponse; @@ -13,15 +15,20 @@ use Zend\Diactoros\Response\HtmlResponse; /** @covers \JKingWeb\Arsse\REST\Microsub\Auth */ class TestAuth extends \JKingWeb\Arsse\Test\AbstractTest { - public function req(string $url, string $method = "GET", array $data = [], array $headers = [], string $type = "application/x-www-form-urlencoded", string $body = null): ResponseInterface { + public function setUp() { + self::clearData(); + Arsse::$db = \Phake::mock(Database::class); + } + + public function req(string $url, string $method = "GET", array $params = [], array $headers = [], array $data = [], string $type = "application/x-www-form-urlencoded", string $body = null, string $user = null): ResponseInterface { $type = (strtoupper($method) === "GET") ? "" : $type; - $req = $this->serverRequest($method, $url, "/u/", $headers, [], $body ?? $data, $type); + $req = $this->serverRequest($method, $url, "/u/", $headers, [], $body ?? $data, $type, $params, $user); return (new \JKingWeb\Arsse\REST\Microsub\Auth)->dispatch($req); } /** @dataProvider provideInvalidRequests */ public function testHandleInvalidRequests(ResponseInterface $exp, string $method, string $url, string $type = null) { - $act = $this->req("http://example.com".$url, $method, [], [], $type ?? ""); + $act = $this->req("http://example.com".$url, $method, [], [], [], $type ?? ""); $this->assertMessage($exp, $act); } @@ -83,4 +90,36 @@ class TestAuth extends \JKingWeb\Arsse\Test\AbstractTest { ["https://example.com:80/u/john.doe", "https://example.com:80"], ]; } + + /** @dataProvider provideLoginData */ + public function testLogInAUser(array $params, string $authenticatedUser = null, ResponseInterface $exp) { + \Phake::when(Arsse::$db)->tokenCreate->thenReturn("authCode"); + $act = $this->req("http://example.com/u/?f=auth", "GET", $params, [], [], "", null, $authenticatedUser); + $this->assertMessage($exp, $act); + if ($act->getStatusCode() == 302 && !preg_match("/\berror=\w/", $act->getHeaderLine("Location") ?? "")) { + \Phake::verify(Arsse::$db)->tokenCreate($authenticatedUser, "microsub.auth", null, null, json_encode([ + 'me' => $params['me'], + 'client_id' => $params['client_id'], + 'redirect_uri' => $params['redirect_uri'], + 'response_type' => strlen($params['response_type'] ?? "") ? $params['response_type'] : "id", + ], \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->tokenCreate; + } + } + + public function provideLoginData() { + return [ + 'Challenge' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], null, new EmptyResponse(401)], + 'Failed challenge' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "", new EmptyResponse(401)], + 'Wrong user 1' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "jane.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=ABCDEF&error=access_denied"])], + 'Wrong user 2' => [['me' => "https://example.com/u/jane.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=ABCDEF&error=access_denied"])], + 'Wrong domain' => [['me' => "https://example.net/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=ABCDEF&error=access_denied"])], + 'Wrong port' => [['me' => "https://example.com:80/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=ABCDEF&error=access_denied"])], + 'Wrong scheme' => [['me' => "ftp://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=ABCDEF&error=access_denied"])], + 'Wrong path' => [['me' => "http://example.com/user/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=ABCDEF&error=access_denied"])], + 'Bad redirect' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "//example.org/redirect", 'state' => "ABCDEF", 'response_type' => "code"], "john.doe", new EmptyResponse(400)], + 'Bad response type' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "ABCDEF", 'response_type' => "bad"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=ABCDEF&error=unsupported_response_type"])], + ]; + } }