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:
parent
8308fbad31
commit
daab0068d6
2 changed files with 83 additions and 9 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
lib/REST/Microsub/ExceptionAuth.php
Normal file
10
lib/REST/Microsub/ExceptionAuth.php
Normal 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 {
|
||||||
|
}
|
Loading…
Reference in a new issue