diff --git a/lib/REST/Microsub/Auth.php b/lib/REST/Microsub/Auth.php
index 3f5f0f27..698807ff 100644
--- a/lib/REST/Microsub/Auth.php
+++ b/lib/REST/Microsub/Auth.php
@@ -11,12 +11,18 @@ use JKingWeb\Arsse\Misc\URL;
use JKingWeb\Arsse\Misc\Date;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
-use Zend\Diactoros\Response\HtmlResponse as Response;
+use Zend\Diactoros\Response\HtmlResponse;
+use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Response\EmptyResponse;
class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
/** The scopes which we grant to Microsub clients. Mute and block are not included because they have no meaning in an RSS/Atom context; this may signal to clients to suppress muting and blocking in their UI */
const SCOPES = "read follow channels";
+ const FUNCTIONS = [
+ 'discovery' => ['GET' => "opDiscovery"],
+ 'login' => ['GET' => "opLogin", 'POST' => "opCodeVerification"],
+ 'issue' => ['POST' => "opIssue"],
+ ];
public function __construct() {
}
@@ -30,11 +36,21 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
}
$id = rawurldecode($id);
// gather the query parameters and act on the "proc" parameter
- $method = "do".ucfirst(strtolower($req->getQueryParams()['proc'] ?? "discovery"));
- if (!method_exists($this, $method)) {
+ $process = $req->getQueryParams()['proc'] ?? "discovery";
+ $method = $req->getMethod();
+ if (isset(self::FUNCTIONS[$process])) {
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";
+ }
+ return new EmptyResponse(204, $fields);
+ } elseif (isset(self::FUNCTIONS[$process][$method])) {
+ return new EmptyResponse(405, ['Allow' => implode(",", array_keys(self::FUNCTIONS[$process]))]);
} else {
- return $this->$method($id, $req);
+ $func = self::FUNCTIONS[$process][$method];
+ return $this->$func($id, $req);
}
}
@@ -63,8 +79,10 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
* Since discovery is publicly accessible, we produce a discovery
* page for all potential user name so as not to facilitate user
* enumeration
+ *
+ * @see https://indieweb.org/Microsub-spec#Discovery
*/
- protected function doDiscovery(string $user, ServerRequestInterface $req): ResponseInterface {
+ protected function opDiscovery(string $user, ServerRequestInterface $req): ResponseInterface {
$base = $this->buildIdentifier($req, true);
$id = $this->buildIdentifier($req);
$urlAuth = $id."?proc=login";
@@ -72,7 +90,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
$urlService = $base."microsub";
// output an extremely basic identity resource
$html = '';
- return new Response($html, 200, [
+ return new HtmlResponse($html, 200, [
"Link: <$urlAuth>; rel=\"authorization_endpoint\"",
"Link: <$urlToken>; rel=\"token_endpoint\"",
"Link: <$urlService>; rel=\"microsub\"",
@@ -85,8 +103,10 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
* challenge; once the user successfully logs in a code is issued
* and redirection occurs. Scopes are for all intents and purposes
* ignored and client information is not presented.
+ *
+ * @see https://indieauth.spec.indieweb.org/#authentication-request
*/
- protected function doLogin(string $user, ServerRequestInterface $req): ResponseInterface {
+ protected function opLogin(string $user, ServerRequestInterface $req): ResponseInterface {
if (!$req->getAttribute("authenticated", false)) {
// user has not yet logged in, or has failed to log in
return new EmptyResponse(401);
@@ -105,15 +125,59 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
if (!URL::absolute($redir)) {
return new EmptyResponse(400);
}
+ // store the client ID and redirect URL
+ $data = json_encode([
+ 'id' => $query['client_id'],
+ 'url' => $query['redirect_uri'],
+ ],\JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
// issue an authorization code and build the redirect URL
- $code = Arsse::$db->tokenCreate($id, "microsub.auth", null, Date::add("PT2M"), $query['client_id']);
+ $code = Arsse::$db->tokenCreate($id, "microsub.auth", null, Date::add("PT2M"), $data);
$next = URL::queryAppend($redir, "code=$code&state=$state");
return new EmptyResponse(302, ["Location: $next"]);
}
}
}
- protected function doIssue(string $user, ServerRequestInterface $req): ResponseInterface {
+ /** Validates an authorization code against client-provided values
+ *
+ * The redirect URL and client ID are checked, as is the user ID
+ *
+ * If everything checks out the canonical user URL is supposed to be returned;
+ * we don't actually know what the canonical URL is modulo URL encoding, but it
+ * doesn't actually matter for our purposes
+ *
+ * @see https://indieauth.spec.indieweb.org/#authorization-code-verification
+ */
+ protected function opCodeVerification(string $user, ServerRequestInterface $req): ResponseInterface {
+ $post = $req->getParsedBody();
+ try {
+ // validate the request parameters
+ $code = $post['code'] ?? "";
+ $id = $post['client_id'] ?? "";
+ $url = $post['redirect_uri'] ?? "";
+ if (!strlen($code) || !strlen($id) || !strlen($url)) {
+ throw new ExceptionAuth("invalid_request");
+ }
+ // check that the token exists
+ $token = Arsse::$db->tokenLookup("microsub.auth", $code);
+ if (!$token) {
+ throw new ExceptionAuth("unsupported_grant_type");
+ }
+ $data = @json_decode($token['data'], true);
+ // validate the token
+ if ($token['user'] !== $user || !is_array($data) || $data['id'] !== $id || $data['url'] !== $url) {
+ throw new ExceptionAuth("unsupported_grant_type");
+ } else {
+ return new JsonResponse(['me' => $this->buildIdentifier($req)]);
+ }
+ } catch (ExceptionAuth $e) {
+ // human-readable error messages could be added, but these must be ASCII per OAuth, so there's probably not much point
+ // see https://tools.ietf.org/html/rfc6749#section-5.2
+ return new JsonResponse(['error' => $e->getMessage()], 400);
+ }
+ }
+ protected function opIssue(string $user, ServerRequestInterface $req): ResponseInterface {
+ $post = $req->getParsedBody();
}
}
diff --git a/lib/REST/Microsub/ExceptionAuth.php b/lib/REST/Microsub/ExceptionAuth.php
new file mode 100644
index 00000000..28816f21
--- /dev/null
+++ b/lib/REST/Microsub/ExceptionAuth.php
@@ -0,0 +1,10 @@
+