Browse Source

First battery of IndieAuth tests, with fixes

microsub
J. King 5 years ago
parent
commit
26fa9461eb
  1. 38
      lib/REST/Microsub/Auth.php
  2. 72
      tests/cases/REST/Microsub/TestAuth.php
  3. 33
      tests/lib/AbstractTest.php

38
lib/REST/Microsub/Auth.php

@ -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'] ?? "",

72
tests/cases/REST/Microsub/TestAuth.php

@ -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"],
];
}
}

33
tests/lib/AbstractTest.php

@ -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…
Cancel
Save