The clean & modern RSS server that doesn't give you any crap. https://thearsse.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

313 lines
33 KiB

<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\REST\Microsub;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\REST\Microsub\Auth;
use JKingWeb\Arsse\REST\Microsub\ExceptionAuth;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse as Response;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\HtmlResponse;
/** @covers \JKingWeb\Arsse\REST\Microsub\Auth<extended> */
class TestAuth extends \JKingWeb\Arsse\Test\AbstractTest {
public function setUp(): void {
self::clearData();
$this->dbMock = $this->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 {
Arsse::$db = $this->dbMock->get();
$type = (strtoupper($method) === "GET") ? "" : $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 ?? "");
$this->assertMessage($exp, $act);
}
public function provideInvalidRequests() {
$r404 = new EmptyResponse(404);
$r405g = new EmptyResponse(405, ['Allow' => "GET"]);
$r405gp = new EmptyResponse(405, ['Allow' => "GET,POST"]);
$r415 = new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"]);
return [
[$r404, "GET", "/u/"],
[$r404, "GET", "/u/john.doe/hello"],
[$r404, "GET", "/u/john.doe/"],
[$r404, "GET", "/u/john.doe?f=hello"],
[$r404, "GET", "/u/?f="],
[$r404, "GET", "/u/?f=goodbye"],
[$r405g, "POST", "/u/john.doe"],
[$r405gp, "PUT", "/u/?f=token"],
[$r404, "POST", "/u/john.doe?f=token"],
[$r415, "POST", "/u/?f=token", "application/json"],
];
}
/** @dataProvider provideOptionsRequests */
public function testHandleOptionsRequests(string $url, array $headerFields) {
$exp = new EmptyResponse(204, $headerFields);
$this->assertMessage($exp, $this->req("http://example.com".$url, "OPTIONS"));
}
public function provideOptionsRequests() {
$ident = ['Allow' => "GET"];
$other = ['Allow' => "GET,POST", 'Accept' => "application/x-www-form-urlencoded"];
return [
["/u/john.doe", $ident],
["/u/?f=token", $other],
["/u/?f=auth", $other],
];
}
/** @dataProvider provideDiscoveryRequests */
public function testDiscoverAUser(string $url, string $origin) {
$auth = $origin."/u/?f=auth";
$token = $origin."/u/?f=token";
$microsub = $origin."/microsub";
$exp = new HtmlResponse('<meta charset="UTF-8"><link rel="authorization_endpoint" href="'.htmlspecialchars($auth).'"><link rel="token_endpoint" href="'.htmlspecialchars($token).'"><link rel="microsub" href="'.htmlspecialchars($microsub).'">', 200, ['Link' => [
"<$auth>; rel=\"authorization_endpoint\"",
"<$token>; rel=\"token_endpoint\"",
"<$microsub>; rel=\"microsub\"",
]]);
$this->assertMessage($exp, $this->req($url));
}
public function provideDiscoveryRequests() {
return [
["http://example.com/u/john.doe", "http://example.com"],
["http://example.com:80/u/john.doe", "http://example.com"],
["https://example.com/u/john.doe", "https://example.com"],
["https://example.com:443/u/john.doe", "https://example.com"],
["http://example.com:443/u/john.doe", "http://example.com:443"],
["https://example.com:80/u/john.doe", "https://example.com:80"],
];
}
/** @dataProvider provideLoginData */
public function testLogInAUser(array $params, string $authenticatedUser = null, ResponseInterface $exp) {
$this->dbMock->tokenCreate->returns("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") ?? "")) {
$this->dbMock->tokenCreate->calledWith($authenticatedUser, "microsub.auth", null, $this->isInstanceOf(\DateTimeInterface::class), 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 {
$this->dbMock->tokenCreate->never()->called();
}
}
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 1' => [['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 domain 2' => [['me' => "https:///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 1' => [['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 redirect 2' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "https:///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"])],
'Success 1' => [['me' => "https://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&code=authCode"])],
'Success 2' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/redirect", 'state' => "R&R", 'response_type' => "code"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/redirect?state=R%26R&code=authCode"])],
'Success 3' => [['me' => "https://example.com/u/john.doe", 'client_id' => "http://example.org/", 'redirect_uri' => "http://example.org/?p=redirect", 'state' => "ABCDEF", 'response_type' => "code"], "john.doe", new EmptyResponse(302, ['Location' => "http://example.org/?p=redirect&state=ABCDEF&code=authCode"])],
];
}
/** @dataProvider provideAuthData */
public function testVerifyAnAuthenticationCode(array $params, string $user, $data, ResponseInterface $exp) {
if ($data instanceof \Exception) {
$this->dbMock->tokenLookup->throws($data);
} else {
$this->dbMock->tokenLookup->returns(['user' => $user, 'data' => $data]);
}
$act = $this->req("http://example.com/u/?f=auth", "POST", [], [], $params);
$this->assertMessage($exp, $act);
if ($act->getStatusCode() == 200) {
$this->dbMock->tokenRevoke->calledWith($user, "microsub.auth", $params['code'] ?? "");
} else {
$this->dbMock->tokenRevoke->never()->called();
}
}
public function provideAuthData() {
return [
'Missing code 1' => [[ 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_request"], 400)],
'Missing code 2' => [['code' => "", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_request"], 400)],
'Missing URL 1' => [['code' => "code", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_request"], 400)],
'Missing URL 2' => [['code' => "code", 'redirect_uri' => "", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_request"], 400)],
'Missing ID 1' => [['code' => "code", 'redirect_uri' => "https://example.org/", ], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_request"], 400)],
'Missing ID 2' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "" ], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_request"], 400)],
'Mismatched URL' => [['code' => "code", 'redirect_uri' => "https://example.net/", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_client" ], 400)],
'Mismatched ID' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.org/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['error' => "invalid_client" ], 400)],
'Bad data 1' => [['code' => "bad-data1", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", null, new Response(['error' => "invalid_grant" ], 400)],
'Bad data 2' => [['code' => "bad-data2", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", '{client_id":"https://example.net/"}', new Response(['error' => "invalid_grant" ], 400)],
'Bad data 3' => [['code' => "bad-data3", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/"}', new Response(['error' => "invalid_grant" ], 400)],
'Bad user' => [['code' => "bad-user", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", new ExceptionInput("subjectMissing"), new Response(['error' => "invalid_grant" ], 400)],
'Bad type' => [['code' => "bad-type", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"token"}', new Response(['error' => "invalid_grant" ], 400)],
'Success 1' => [['code' => "valid-code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "someone", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/"}', new Response(['me' => "http://example.com/u/someone"], 200)],
'Success 2' => [['code' => "good-code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/"], "somehow", '{"redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"id"}', new Response(['me' => "http://example.com/u/somehow"], 200)],
];
}
/** @dataProvider provideTokenRequests */
public function testIssueAnAccessToken(array $params, string $user, $data, ResponseInterface $exp) {
if ($data instanceof \Exception) {
$this->dbMock->tokenLookup->throws($data);
} else {
$this->dbMock->tokenLookup->returns(['user' => $user, 'data' => $data]);
}
$this->dbMock->tokenCreate->returns("TOKEN");
$act = $this->req("http://example.com/u/?f=token", "POST", [], [], $params);
$this->assertMessage($exp, $act);
if ($act->getStatusCode() == 200) {
$input = '{"me":"'.($params['me'] ?? "").'","client_id":"'.($params['client_id'] ?? "").'"}';
$this->dbMock->tokenCreate->calledWith($user, "microsub.access", null, null, $input);
$this->dbMock->tokenRevoke->calledWith($user, "microsub.auth", $params['code'] ?? "");
} else {
$this->dbMock->tokenCreate->never()->called();
$this->dbMock->tokenRevoke->never()->called();
}
}
public function provideTokenRequests() {
$scopes = implode(" ", Auth::SCOPES);
return [
'Missing code 1' => [[ 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Missing code 2' => [['code' => "", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Missing URL 1' => [['code' => "code", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Missing URL 2' => [['code' => "code", 'redirect_uri' => "", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Missing ID 1' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Missing ID 2' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Missing grant 1' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "unsupported_grant_type"], 400)],
'Missing grant 2' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "unsupported_grant_type"], 400)],
'Missing me 1' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "" ], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Missing me 2' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", ], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_request" ], 400)],
'Mismatched URL' => [['code' => "code", 'redirect_uri' => "https://example.net/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_client" ], 400)],
'Mismatched ID' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.org/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_client" ], 400)],
'Mismatched grant' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "mismatch", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "unsupported_grant_type"], 400)],
'Mismatched me' => [['code' => "code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/" ], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_grant" ], 400)],
'Bad data 1' => [['code' => "bad-data1", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", null, new Response(['error' => "invalid_grant" ], 400)],
'Bad data 2' => [['code' => "bad-data2", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{ "redirect_uri":"https://example.org/", client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_grant" ], 400)],
'Bad data 3' => [['code' => "bad-data3", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone", client_id":"https://example.net/","response_type":"code"}', new Response(['error' => "invalid_grant" ], 400)],
'Bad data 4' => [['code' => "bad-data4", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/", "response_type":"code"}', new Response(['error' => "invalid_grant" ], 400)],
'Bad data 5' => [['code' => "bad-data5", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/", }', new Response(['error' => "invalid_grant" ], 400)],
'Bad data 6' => [['code' => "bad-data6", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"id" }', new Response(['error' => "invalid_grant" ], 400)],
'Bad user' => [['code' => "bad-user", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", new ExceptionInput("subjectMissing"), new Response(['error' => "invalid_grant" ], 400)],
'Success 1' => [['code' => "valid-code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/someone"], "someone", '{"me":"https://example.com/u/someone","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['me' => "http://example.com/u/someone", 'token_type' => "Bearer", 'access_token' => "TOKEN", 'scope' => $scopes], 200)],
'Success 2' => [['code' => "good-code", 'redirect_uri' => "https://example.org/", 'client_id' => "https://example.net/", 'grant_type' => "authorization_code", 'me' => "https://example.com/u/somehow"], "somehow", '{"me":"https://example.com/u/somehow","redirect_uri":"https://example.org/","client_id":"https://example.net/","response_type":"code"}', new Response(['me' => "http://example.com/u/somehow", 'token_type' => "Bearer", 'access_token' => "TOKEN", 'scope' => $scopes], 200)],
];
}
/** @dataProvider provideBearers */
public function testLogInABearer(string $authorization, array $scopes, string $token, string $user, $data, $exp) {
if ($data instanceof \Exception) {
$this->dbMock->tokenLookup->throws($data);
} else {
$this->dbMock->tokenLookup->returns(['user' => $user, 'data' => $data]);
}
Arsse::$db = $this->dbMock->get();
if ($exp instanceof \Exception) {
$this->assertException($exp);
}
try {
$act = Auth::validateBearer($authorization, $scopes);
$this->assertSame($exp, $act);
} finally {
if (strlen($token)) {
$this->dbMock->tokenLookup->calledWith("microsub.access", $token);
} else {
$this->dbMock->tokenLookup->never()->called();
}
}
}
public function provideBearers() {
return [
'Not a bearer' => ["Beaver TOKEN", [], "", "", "", new ExceptionAuth("invalid_request")],
'Token missing 1' => ["Bearer", [], "", "", "", new ExceptionAuth("invalid_request")],
'Token missing 2' => ["Bearer ", [], "", "", "", new ExceptionAuth("invalid_request")],
'Not a token' => ["Bearer !", [], "", "", "", new ExceptionAuth("invalid_request")],
'Invalid token' => ["Bearer TOKEN", [], "TOKEN", "", new ExceptionInput("subjectMissing"), new ExceptionAuth("invalid_token")],
'Insufficient scope 1' => ["Bearer TOKEN", ["missing"], "TOKEN", "someone", null, new ExceptionAuth("insufficient_scope")],
'Insufficient scope 2' => ["Bearer TOKEN", ["channels"], "TOKEN", "someone", '{"scope":["read","follow"]}', new ExceptionAuth("insufficient_scope")],
'Success 1' => ["Bearer TOKEN", [], "TOKEN", "someone", null, ["someone", ['scope' => Auth::SCOPES]]],
'Success 2' => ["bearer TOKEN", [], "TOKEN", "someone", null, ["someone", ['scope' => Auth::SCOPES]]],
'Success 3' => ["BEARER TOKEN", [], "TOKEN", "someone", null, ["someone", ['scope' => Auth::SCOPES]]],
'Broken data' => ["Bearer TOKEN", [], "TOKEN", "someone", '{', ["someone", ['scope' => Auth::SCOPES]]],
];
}
/** @dataProvider provideRevocations */
public function testRevokeAToken(array $params, $user, ResponseInterface $exp) {
$this->dbMock->tokenRevoke->returns(true);
if ($user instanceof \Exception) {
$this->dbMock->tokenLookup->throws($user);
} else {
$this->dbMock->tokenLookup->returns(['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) {
$this->dbMock->tokenLookup->calledWith("microsub.access", $params['token'] ?? "");
} else {
$this->dbMock->tokenLookup->never()->called();
}
if ($doRevoke) {
$this->dbMock->tokenRevoke->calledWith($user, "microsub.access", $params['token'] ?? "");
} else {
$this->dbMock->tokenRevoke->never()->called();
}
}
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) {
$this->dbMock->tokenLookup->throws($output);
} else {
$this->dbMock->tokenLookup->returns(['user' => "someone", 'data' => $output]);
}
$this->assertMessage($exp, $this->req("http://example.com/u/?f=token", "GET", [], $authorization ? ['Authorization' => $authorization] : []));
$this->dbMock->tokenRevoke->never()->called();
}
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"])],
];
}
}