1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-23 09:02:41 +00:00

Auth code verification and general reorganization

This commit is contained in:
J. King 2019-09-14 18:44:40 -04:00
parent 8308fbad31
commit daab0068d6
2 changed files with 83 additions and 9 deletions

View file

@ -11,12 +11,18 @@ use JKingWeb\Arsse\Misc\URL;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
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 as Response; use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
class Auth extends \JKingWeb\Arsse\REST\AbstractHandler { 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 */ /** 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 SCOPES = "read follow channels";
const FUNCTIONS = [
'discovery' => ['GET' => "opDiscovery"],
'login' => ['GET' => "opLogin", 'POST' => "opCodeVerification"],
'issue' => ['POST' => "opIssue"],
];
public function __construct() { public function __construct() {
} }
@ -30,11 +36,21 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
} }
$id = rawurldecode($id); $id = rawurldecode($id);
// gather the query parameters and act on the "proc" parameter // gather the query parameters and act on the "proc" parameter
$method = "do".ucfirst(strtolower($req->getQueryParams()['proc'] ?? "discovery")); $process = $req->getQueryParams()['proc'] ?? "discovery";
if (!method_exists($this, $method)) { $method = $req->getMethod();
if (isset(self::FUNCTIONS[$process])) {
return new EmptyResponse(404); 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 { } 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 * Since discovery is publicly accessible, we produce a discovery
* page for all potential user name so as not to facilitate user * page for all potential user name so as not to facilitate user
* enumeration * 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); $base = $this->buildIdentifier($req, true);
$id = $this->buildIdentifier($req); $id = $this->buildIdentifier($req);
$urlAuth = $id."?proc=login"; $urlAuth = $id."?proc=login";
@ -72,7 +90,7 @@ 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 Response($html, 200, [ return new HtmlResponse($html, 200, [
"Link: <$urlAuth>; rel=\"authorization_endpoint\"", "Link: <$urlAuth>; rel=\"authorization_endpoint\"",
"Link: <$urlToken>; rel=\"token_endpoint\"", "Link: <$urlToken>; rel=\"token_endpoint\"",
"Link: <$urlService>; rel=\"microsub\"", "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 * challenge; once the user successfully logs in a code is issued
* and redirection occurs. Scopes are for all intents and purposes * and redirection occurs. Scopes are for all intents and purposes
* ignored and client information is not presented. * 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)) { if (!$req->getAttribute("authenticated", false)) {
// user has not yet logged in, or has failed to log in // user has not yet logged in, or has failed to log in
return new EmptyResponse(401); return new EmptyResponse(401);
@ -105,15 +125,59 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
if (!URL::absolute($redir)) { if (!URL::absolute($redir)) {
return new EmptyResponse(400); 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 // 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"); $next = URL::queryAppend($redir, "code=$code&state=$state");
return new EmptyResponse(302, ["Location: $next"]); 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();
} }
} }

View file

@ -0,0 +1,10 @@
<?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\REST\Microsub;
class ExceptionAuth extends \Exception {
}