mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2025-01-09 09:22:40 +00:00
First battery of IndieAuth tests, with fixes
This commit is contained in:
parent
6d2b587e38
commit
26fa9461eb
3 changed files with 125 additions and 18 deletions
|
@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\REST\Microsub;
|
|||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Misc\URL;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
|
@ -24,13 +25,21 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
'auth' => ['GET' => "opLogin", 'POST' => "opCodeVerification"],
|
||||
'token' => ['GET' => "opTokenVerification", 'POST' => "opIssueAccessToken"],
|
||||
];
|
||||
/** The minimal set of reserved URL characters which must be escaped when comparing user ID URLs */
|
||||
/** The minimal set of reserved URL characters which mus t be escaped when comparing user ID URLs */
|
||||
const USERNAME_ESCAPES = [
|
||||
'#' => "%23",
|
||||
'%' => "%25",
|
||||
'/' => "%2F",
|
||||
'?' => "%3F",
|
||||
];
|
||||
/** The minimal set of reserved URL characters which must be escaped in query values */
|
||||
const QUERY_ESCAPES = [
|
||||
'#' => "%23",
|
||||
'%' => "%25",
|
||||
'&' => "%26",
|
||||
];
|
||||
/** The acceptable media type of input for POST requests */
|
||||
const ACCEPTED_TYPES = "application/x-www-form-urlencoded";
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
@ -44,19 +53,25 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
// gather the query parameters and act on the "f" (function) parameter
|
||||
$process = $req->getQueryParams()['f'] ?? "";
|
||||
$method = $req->getMethod();
|
||||
if (isset(self::FUNCTIONS[$process]) || ($process === "" && !strlen($path)) || ($process !== "" && strlen($path))) {
|
||||
if (!isset(self::FUNCTIONS[$process]) || ($process === "" && !strlen($path)) || ($process !== "" && strlen($path))) {
|
||||
// the function requested needs to exist
|
||||
// the path should also be empty unless we're doing discovery
|
||||
return new EmptyResponse(404);
|
||||
} elseif ($method === "OPTIONS") {
|
||||
$fields = ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))];
|
||||
if (isset(self::FUNCTIONS[$process]['POST'])) {
|
||||
$fields['Accept'] = "application/x-www-form-urlencoded";
|
||||
$fields['Accept'] = self::ACCEPTED_TYPES;
|
||||
}
|
||||
return new EmptyResponse(204, $fields);
|
||||
} elseif (isset(self::FUNCTIONS[$process][$method])) {
|
||||
} elseif (!isset(self::FUNCTIONS[$process][$method])) {
|
||||
return new EmptyResponse(405, ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))]);
|
||||
} else {
|
||||
if ($req->getMethod() !== "GET") {
|
||||
$type = $req->getHeaderLine("Content-Type") ?? "";
|
||||
if (strlen($type) && strtolower($type) !== self::ACCEPTED_TYPES) {
|
||||
return new EmptyResponse(415, ['Accept' => self::ACCEPTED_TYPES]);
|
||||
}
|
||||
}
|
||||
try {
|
||||
$func = self::FUNCTIONS[$process][$method];
|
||||
return $this->$func($req);
|
||||
|
@ -76,8 +91,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
protected function buildBaseURL(ServerRequestInterface $req): string {
|
||||
// construct the base user identifier URL; the user is never checked against the database
|
||||
$s = $req->getServerParams();
|
||||
$path = $req->getRequestTarget()['path'];
|
||||
$https = (strlen($s['HTTPS'] ?? "") && $s['HTTPS'] !== "off");
|
||||
$https = ValueInfo::normalize($s['HTTPS'] ?? "", ValueInfo::T_BOOL);
|
||||
$port = (int) ($s['SERVER_PORT'] ?? 0);
|
||||
$port = (!$port || ($https && $port == 443) || (!$https && $port == 80)) ? "" : ":$port";
|
||||
return URL::normalize(($https ? "https" : "http")."://".$s['HTTP_HOST'].$port."/");
|
||||
|
@ -149,11 +163,11 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
$urlService = $base."microsub";
|
||||
// output an extremely basic identity resource
|
||||
$html = '<meta charset="UTF-8"><link rel="authorization_endpoint" href="'.htmlspecialchars($urlAuth).'"><link rel="token_endpoint" href="'.htmlspecialchars($urlToken).'"><link rel="microsub" href="'.htmlspecialchars($urlService).'">';
|
||||
return new HtmlResponse($html, 200, [
|
||||
"Link: <$urlAuth>; rel=\"authorization_endpoint\"",
|
||||
"Link: <$urlToken>; rel=\"token_endpoint\"",
|
||||
"Link: <$urlService>; rel=\"microsub\"",
|
||||
]);
|
||||
return new HtmlResponse($html, 200, ['Link' => [
|
||||
"<$urlAuth>; rel=\"authorization_endpoint\"",
|
||||
"<$urlToken>; rel=\"token_endpoint\"",
|
||||
"<$urlService>; rel=\"microsub\"",
|
||||
]]);
|
||||
}
|
||||
|
||||
/** Handles the authentication process
|
||||
|
@ -311,7 +325,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
'invalid_request' => 400,
|
||||
'invalid_token' => 401,
|
||||
][$errCode] ?? 500;
|
||||
return new EmptyResponse($httpCode, ['WWW-Authenticate' => "Bearer error=\"$erroCode\""]);
|
||||
return new EmptyResponse($httpCode, ['WWW-Authenticate' => "Bearer error=\"$errCode\""]);
|
||||
}
|
||||
return new JsonResponse([
|
||||
'me' => $data['me'] ?? "",
|
||||
|
|
|
@ -7,10 +7,80 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\Arsse\TestCase\REST\Microsub;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
use Zend\Diactoros\Response\JsonResponse as Response;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST\Microsub\Auth<extended> */
|
||||
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 {
|
||||
$type = (strtoupper($method) === "GET") ? "" : $type;
|
||||
$req = $this->serverRequest($method, $url, "/u/", $headers, [], $body ?? $data, $type);
|
||||
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"],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,13 +64,12 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
}
|
||||
|
||||
protected function serverRequest(string $method, string $url, string $urlPrefix, array $headers = [], array $vars = [], $body = null, string $type = "", $params = [], string $user = null): ServerRequestInterface {
|
||||
$server = [
|
||||
'REQUEST_METHOD' => $method,
|
||||
'REQUEST_URI' => $url,
|
||||
];
|
||||
// build an initial $_SERVER array
|
||||
$server = ['REQUEST_METHOD' => strtoupper($method)];
|
||||
if (strlen($type)) {
|
||||
$server['HTTP_CONTENT_TYPE'] = $type;
|
||||
}
|
||||
// add any specified parameters to the URL
|
||||
if (isset($params)) {
|
||||
if (is_array($params)) {
|
||||
$params = implode("&", array_map(function($v, $k) {
|
||||
|
@ -79,12 +78,27 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
}
|
||||
$url = URL::queryAppend($url, (string) $params);
|
||||
}
|
||||
// glean the scheme, hostname, and port from the URL
|
||||
$urlParts = parse_url($url);
|
||||
if (isset($urlParts['scheme'])) {
|
||||
$server['HTTPS'] = strtolower($urlParts['scheme']) === "https" ? "on" : "off";
|
||||
}
|
||||
if (isset($urlParts['host'])) {
|
||||
$server['HTTP_HOST'] = $urlParts['host'];
|
||||
if (isset($urlParts['port'])) {
|
||||
$server['SERVER_PORT'] = $urlParts['port'];
|
||||
}
|
||||
$url = $urlParts['path'].(isset($urlParts['query']) ? "?".$urlParts['query'] : "");
|
||||
}
|
||||
$server['REQUEST_URI'] = $url;
|
||||
// rebuild the parsed query parameters from the URL
|
||||
$q = parse_url($url, \PHP_URL_QUERY);
|
||||
if (strlen($q ?? "")) {
|
||||
parse_str($q, $params);
|
||||
} else {
|
||||
$params = [];
|
||||
}
|
||||
// prepare the body and parsed body
|
||||
$parsedBody = null;
|
||||
if (isset($body)) {
|
||||
if (is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) {
|
||||
|
@ -96,8 +110,12 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
$body = http_build_query($body, "a", "&");
|
||||
}
|
||||
}
|
||||
// add any override values to the $_SERVER array
|
||||
$server = array_merge($server, $vars);
|
||||
// create the request
|
||||
$req = new ServerRequest($server, [], $url, $method, "php://memory", [], [], $params, $parsedBody);
|
||||
// if a user is specified, act as if they were authenticated by the global handler
|
||||
// the empty string denotes failed authentication
|
||||
if (isset($user)) {
|
||||
if (strlen($user)) {
|
||||
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user);
|
||||
|
@ -105,9 +123,11 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
$req = $req->withAttribute("authenticationFailed", true);
|
||||
}
|
||||
}
|
||||
if (strlen($type) &&strlen($body ?? "")) {
|
||||
// if a content type was specified, add it as a header
|
||||
if (strlen($type)) {
|
||||
$req = $req->withHeader("Content-Type", $type);
|
||||
}
|
||||
// add any other headers
|
||||
foreach ($headers as $key => $value) {
|
||||
if (!is_null($value)) {
|
||||
$req = $req->withHeader($key, $value);
|
||||
|
@ -115,13 +135,16 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
$req = $req->withoutHeader($key);
|
||||
}
|
||||
}
|
||||
// strip the URL prefix from the request target, as the global handler does
|
||||
$target = substr(URL::normalize($url), strlen($urlPrefix));
|
||||
$req = $req->withRequestTarget($target);
|
||||
// add the body text, if any
|
||||
if (strlen($body ?? "")) {
|
||||
$p = $req->getBody();
|
||||
$p->write($body);
|
||||
$req = $req->withBody($p);
|
||||
}
|
||||
// return the prepared request
|
||||
return $req;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue