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\Arsse;
|
||||||
use JKingWeb\Arsse\Misc\URL;
|
use JKingWeb\Arsse\Misc\URL;
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Zend\Diactoros\Response\HtmlResponse;
|
use Zend\Diactoros\Response\HtmlResponse;
|
||||||
|
@ -24,13 +25,21 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
'auth' => ['GET' => "opLogin", 'POST' => "opCodeVerification"],
|
'auth' => ['GET' => "opLogin", 'POST' => "opCodeVerification"],
|
||||||
'token' => ['GET' => "opTokenVerification", 'POST' => "opIssueAccessToken"],
|
'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 = [
|
const USERNAME_ESCAPES = [
|
||||||
'#' => "%23",
|
'#' => "%23",
|
||||||
'%' => "%25",
|
'%' => "%25",
|
||||||
'/' => "%2F",
|
'/' => "%2F",
|
||||||
'?' => "%3F",
|
'?' => "%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() {
|
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
|
// gather the query parameters and act on the "f" (function) parameter
|
||||||
$process = $req->getQueryParams()['f'] ?? "";
|
$process = $req->getQueryParams()['f'] ?? "";
|
||||||
$method = $req->getMethod();
|
$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 function requested needs to exist
|
||||||
// the path should also be empty unless we're doing discovery
|
// the path should also be empty unless we're doing discovery
|
||||||
return new EmptyResponse(404);
|
return new EmptyResponse(404);
|
||||||
} elseif ($method === "OPTIONS") {
|
} elseif ($method === "OPTIONS") {
|
||||||
$fields = ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))];
|
$fields = ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))];
|
||||||
if (isset(self::FUNCTIONS[$process]['POST'])) {
|
if (isset(self::FUNCTIONS[$process]['POST'])) {
|
||||||
$fields['Accept'] = "application/x-www-form-urlencoded";
|
$fields['Accept'] = self::ACCEPTED_TYPES;
|
||||||
}
|
}
|
||||||
return new EmptyResponse(204, $fields);
|
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]))]);
|
return new EmptyResponse(405, ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))]);
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
$func = self::FUNCTIONS[$process][$method];
|
$func = self::FUNCTIONS[$process][$method];
|
||||||
return $this->$func($req);
|
return $this->$func($req);
|
||||||
|
@ -76,8 +91,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
protected function buildBaseURL(ServerRequestInterface $req): string {
|
protected function buildBaseURL(ServerRequestInterface $req): string {
|
||||||
// construct the base user identifier URL; the user is never checked against the database
|
// construct the base user identifier URL; the user is never checked against the database
|
||||||
$s = $req->getServerParams();
|
$s = $req->getServerParams();
|
||||||
$path = $req->getRequestTarget()['path'];
|
$https = ValueInfo::normalize($s['HTTPS'] ?? "", ValueInfo::T_BOOL);
|
||||||
$https = (strlen($s['HTTPS'] ?? "") && $s['HTTPS'] !== "off");
|
|
||||||
$port = (int) ($s['SERVER_PORT'] ?? 0);
|
$port = (int) ($s['SERVER_PORT'] ?? 0);
|
||||||
$port = (!$port || ($https && $port == 443) || (!$https && $port == 80)) ? "" : ":$port";
|
$port = (!$port || ($https && $port == 443) || (!$https && $port == 80)) ? "" : ":$port";
|
||||||
return URL::normalize(($https ? "https" : "http")."://".$s['HTTP_HOST'].$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";
|
$urlService = $base."microsub";
|
||||||
// output an extremely basic identity resource
|
// 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).'">';
|
$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, [
|
return new HtmlResponse($html, 200, ['Link' => [
|
||||||
"Link: <$urlAuth>; rel=\"authorization_endpoint\"",
|
"<$urlAuth>; rel=\"authorization_endpoint\"",
|
||||||
"Link: <$urlToken>; rel=\"token_endpoint\"",
|
"<$urlToken>; rel=\"token_endpoint\"",
|
||||||
"Link: <$urlService>; rel=\"microsub\"",
|
"<$urlService>; rel=\"microsub\"",
|
||||||
]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handles the authentication process
|
/** Handles the authentication process
|
||||||
|
@ -311,7 +325,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
'invalid_request' => 400,
|
'invalid_request' => 400,
|
||||||
'invalid_token' => 401,
|
'invalid_token' => 401,
|
||||||
][$errCode] ?? 500;
|
][$errCode] ?? 500;
|
||||||
return new EmptyResponse($httpCode, ['WWW-Authenticate' => "Bearer error=\"$erroCode\""]);
|
return new EmptyResponse($httpCode, ['WWW-Authenticate' => "Bearer error=\"$errCode\""]);
|
||||||
}
|
}
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'me' => $data['me'] ?? "",
|
'me' => $data['me'] ?? "",
|
||||||
|
|
|
@ -7,10 +7,80 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\TestCase\REST\Microsub;
|
namespace JKingWeb\Arsse\TestCase\REST\Microsub;
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Zend\Diactoros\ServerRequest;
|
|
||||||
use Zend\Diactoros\Response\JsonResponse as Response;
|
use Zend\Diactoros\Response\JsonResponse as Response;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
|
use Zend\Diactoros\Response\HtmlResponse;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\REST\Microsub\Auth<extended> */
|
/** @covers \JKingWeb\Arsse\REST\Microsub\Auth<extended> */
|
||||||
class TestAuth extends \JKingWeb\Arsse\Test\AbstractTest {
|
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 {
|
protected function serverRequest(string $method, string $url, string $urlPrefix, array $headers = [], array $vars = [], $body = null, string $type = "", $params = [], string $user = null): ServerRequestInterface {
|
||||||
$server = [
|
// build an initial $_SERVER array
|
||||||
'REQUEST_METHOD' => $method,
|
$server = ['REQUEST_METHOD' => strtoupper($method)];
|
||||||
'REQUEST_URI' => $url,
|
|
||||||
];
|
|
||||||
if (strlen($type)) {
|
if (strlen($type)) {
|
||||||
$server['HTTP_CONTENT_TYPE'] = $type;
|
$server['HTTP_CONTENT_TYPE'] = $type;
|
||||||
}
|
}
|
||||||
|
// add any specified parameters to the URL
|
||||||
if (isset($params)) {
|
if (isset($params)) {
|
||||||
if (is_array($params)) {
|
if (is_array($params)) {
|
||||||
$params = implode("&", array_map(function($v, $k) {
|
$params = implode("&", array_map(function($v, $k) {
|
||||||
|
@ -79,12 +78,27 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
||||||
}
|
}
|
||||||
$url = URL::queryAppend($url, (string) $params);
|
$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);
|
$q = parse_url($url, \PHP_URL_QUERY);
|
||||||
if (strlen($q ?? "")) {
|
if (strlen($q ?? "")) {
|
||||||
parse_str($q, $params);
|
parse_str($q, $params);
|
||||||
} else {
|
} else {
|
||||||
$params = [];
|
$params = [];
|
||||||
}
|
}
|
||||||
|
// prepare the body and parsed body
|
||||||
$parsedBody = null;
|
$parsedBody = null;
|
||||||
if (isset($body)) {
|
if (isset($body)) {
|
||||||
if (is_string($body) && in_array(strtolower($type), ["", "application/x-www-form-urlencoded"])) {
|
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", "&");
|
$body = http_build_query($body, "a", "&");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// add any override values to the $_SERVER array
|
||||||
$server = array_merge($server, $vars);
|
$server = array_merge($server, $vars);
|
||||||
|
// create the request
|
||||||
$req = new ServerRequest($server, [], $url, $method, "php://memory", [], [], $params, $parsedBody);
|
$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 (isset($user)) {
|
||||||
if (strlen($user)) {
|
if (strlen($user)) {
|
||||||
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $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);
|
$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);
|
$req = $req->withHeader("Content-Type", $type);
|
||||||
}
|
}
|
||||||
|
// add any other headers
|
||||||
foreach ($headers as $key => $value) {
|
foreach ($headers as $key => $value) {
|
||||||
if (!is_null($value)) {
|
if (!is_null($value)) {
|
||||||
$req = $req->withHeader($key, $value);
|
$req = $req->withHeader($key, $value);
|
||||||
|
@ -115,13 +135,16 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
||||||
$req = $req->withoutHeader($key);
|
$req = $req->withoutHeader($key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// strip the URL prefix from the request target, as the global handler does
|
||||||
$target = substr(URL::normalize($url), strlen($urlPrefix));
|
$target = substr(URL::normalize($url), strlen($urlPrefix));
|
||||||
$req = $req->withRequestTarget($target);
|
$req = $req->withRequestTarget($target);
|
||||||
|
// add the body text, if any
|
||||||
if (strlen($body ?? "")) {
|
if (strlen($body ?? "")) {
|
||||||
$p = $req->getBody();
|
$p = $req->getBody();
|
||||||
$p->write($body);
|
$p->write($body);
|
||||||
$req = $req->withBody($p);
|
$req = $req->withBody($p);
|
||||||
}
|
}
|
||||||
|
// return the prepared request
|
||||||
return $req;
|
return $req;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue