mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2025-01-09 09:22:40 +00:00
Refactor auth code verification some more, and fix token issuance
This commit is contained in:
parent
7a337d7d62
commit
e6482bb8aa
1 changed files with 77 additions and 45 deletions
|
@ -84,10 +84,29 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
return URL::normalize(($https ? "https" : "http")."://".$s['HTTP_HOST'].$port."/");
|
||||
}
|
||||
|
||||
/** Produces a canoncial identity URL based on a server request and a user name
|
||||
*
|
||||
* This involves reconstructing the scheme and authority based on $_SERVER
|
||||
* variables; it may fail depending on server configuration
|
||||
*/
|
||||
protected function buildIdentifier(ServerRequestInterface $req, string $user): string {
|
||||
return $this->buildBaseURL($req)."u/".str_replace(array_keys(self::USERNAME_ESCAPES), array_values(self::USERNAME_ESCAPES), $user);
|
||||
}
|
||||
|
||||
/** Matches an identity URL against its canoncial form
|
||||
*
|
||||
* The identifier matches if all of the following are true:
|
||||
*
|
||||
* 1. The scheme is http or https
|
||||
* 2. The normalized hostname matches
|
||||
* 3. The port matches after dropping default port numbers
|
||||
* 4. No credentials are included in the authority
|
||||
* 5. The path is `/u/<username>`
|
||||
* 6. There is no query content
|
||||
* 7. The username, when URL-decoded, matches
|
||||
*
|
||||
* Though IndieAuth forbids port numbers and fragments in identifiers, we do not enforce this
|
||||
*/
|
||||
protected function matchIdentifier(string $canonical, string $me): bool {
|
||||
$me = parse_url(URL::normalize($me));
|
||||
$me['scheme'] = $me['scheme'] ?? "";
|
||||
|
@ -138,7 +157,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
]);
|
||||
}
|
||||
|
||||
/** Handles the authentication/authorization process
|
||||
/** Handles the authentication process
|
||||
*
|
||||
* Authentication is achieved via an HTTP Basic authentiation
|
||||
* challenge; once the user successfully logs in a code is issued
|
||||
|
@ -146,6 +165,7 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
* ignored and client information is not presented.
|
||||
*
|
||||
* @see https://indieauth.spec.indieweb.org/#authentication-request
|
||||
* @see https://indieauth.spec.indieweb.org/#authorization-endpoint-0
|
||||
*/
|
||||
protected function opLogin(ServerRequestInterface $req): ResponseInterface {
|
||||
if (!$req->getAttribute("authenticated", false)) {
|
||||
|
@ -188,68 +208,80 @@ class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/** Validates an authorization code against client-provided values
|
||||
/** Handles the auth code verification of the basic "Authentication" flow of IndieAuth
|
||||
*
|
||||
* 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
|
||||
* This is not used by Microsub
|
||||
*
|
||||
* @see https://indieauth.spec.indieweb.org/#authorization-code-verification
|
||||
* @see https://indieauth.spec.indieweb.org/#authorization-code-verification-0
|
||||
*/
|
||||
protected function opCodeVerification(ServerRequestInterface $req): ResponseInterface {
|
||||
$post = $req->getParsedBody();
|
||||
// validate the request parameters
|
||||
$code = $post['code'] ?? "";
|
||||
$client = $post['client_id'] ?? "";
|
||||
$redir = $post['redirect_uri'] ?? "";
|
||||
if (!strlen($code) || !strlen($client) || !strlen($redir)) {
|
||||
$tr = Arsse::$db->begin();
|
||||
// validate the request parameters; an exception will be thrown if not
|
||||
list($user, $type) = $this->validateAuthCode($post['code'] ?? "", $post['client_id'] ?? "", $post['redirect_uri'] ?? "");
|
||||
if ($type !== "id") {
|
||||
throw new ExceptionAuth("invalid_grant");
|
||||
}
|
||||
// delete the auth code since it is valid and may only be used once
|
||||
Arsse::$db->tokenRevoke($user, "microsub.auth", $post['code']);
|
||||
$tr->commit();
|
||||
// return the canonical identity URL
|
||||
return new JsonResponse(['me' => $this->buildIdentifier($req, $user)]);
|
||||
}
|
||||
|
||||
/** Handles the auth code verification and token issuance of the "Authorization" flow of IndieAuth
|
||||
*
|
||||
* @see https://indieauth.spec.indieweb.org/#token-endpoint-0
|
||||
*/
|
||||
protected function opIssueAccessToken(ServerRequestInterface $req): ResponseInterface {
|
||||
$post = $req->getParsedBody();
|
||||
if (($post['grant_type'] ?? "") !== "authorization_code") {
|
||||
throw new ExceptionAuth("unsupported_grant_type");
|
||||
}
|
||||
$tr = Arsse::$db->begin();
|
||||
list($user, $type) = $this->validateAuthCode($post['code'] ?? "", $post['client_id'] ?? "", $post['redirect_url'] ?? "", $post['me'] ?? "");
|
||||
if ($type !== "code") {
|
||||
throw new ExceptionAuth("invalid_grant");
|
||||
}
|
||||
// issue an access token
|
||||
$token = Arsse::$db->tokenCreate($user, "microsub.access");
|
||||
Arsse::$db->tokenRevoke($user, "microsub.auth", $post['code']);
|
||||
$tr->commit();
|
||||
// return the Bearer token and associated data
|
||||
return new JsonResponse([
|
||||
'me' => $this->buildIdentifier($req, $user),
|
||||
'token_type' => "Bearer",
|
||||
'access_token' => $token,
|
||||
'scope' => self::SCOPES,
|
||||
]);
|
||||
}
|
||||
|
||||
/** Validates an auth code and throws appropriate exceptions otherwise
|
||||
*
|
||||
* Returns an indexed araay containing the username and the grant type (either "id" or "code")
|
||||
*
|
||||
* It is the responsibility of the calling function to revoke the auth code if the code is accepted
|
||||
*/
|
||||
protected function validateAuthCode(string $code, string $clientId, string $redirUrl, string $me = null): array {
|
||||
if (!strlen($code) || !strlen($clientId) || !strlen($redirUrl)) {
|
||||
throw new ExceptionAuth("invalid_request");
|
||||
}
|
||||
// check that the token exists
|
||||
// check that the auth code exists
|
||||
$token = Arsse::$db->tokenLookup("microsub.auth", $code);
|
||||
if (!$token) {
|
||||
throw new ExceptionAuth("invalid_grant");
|
||||
}
|
||||
$data = @json_decode($token['data'], true);
|
||||
// validate the token
|
||||
// validate the auth code
|
||||
if (!is_array($data)) {
|
||||
throw new ExceptionAuth("invalid_grant");
|
||||
} elseif ($data['client'] !== $client || $data['redir'] !== $redir) {
|
||||
} elseif ($data['client'] !== $clientId || $data['redir'] !== $redirUrl) {
|
||||
throw new ExceptionAuth("invalid_client");
|
||||
} else {
|
||||
$out = ['me' => $this->buildIdentifier($req, $token['user'])];
|
||||
if ($data['type'] === "code") {
|
||||
$out['scope'] = self::SCOPES;
|
||||
}
|
||||
return new JsonResponse($out);
|
||||
}
|
||||
}
|
||||
|
||||
protected function opIssueAccessToken(ServerRequestInterface $req): ResponseInterface {
|
||||
$post = $req->getParsedBody();
|
||||
$type = $post['grant_type'] ?? "";
|
||||
$me = $post['me'] ?? "";
|
||||
if ($type !== "authorization_code") {
|
||||
throw new ExceptionAuth("unsupported_grant_type");
|
||||
} elseif ($this->buildIdentifier($req) !== $me) {
|
||||
} elseif (isset($me) && $me !== $data['me']) {
|
||||
throw new ExceptionAuth("invalid_grant");
|
||||
} else {
|
||||
$out = $this->opCodeVerification($user, $req)->getPayload();
|
||||
if (!isset($out['scope'])) {
|
||||
throw new ExceptionAuth("invalid_scope");
|
||||
}
|
||||
// issue an access token
|
||||
$tr = Arsse::$db->begin();
|
||||
$token = Arsse::$db->tokenCreate($user, "microsub.access");
|
||||
Arsse::$db->tokenRevoke($user, "microsub.auth", $post['code']);
|
||||
$tr->commit();
|
||||
$out['access_token'] = $token;
|
||||
$out['token_type'] = "Bearer";
|
||||
return new JsonResponse($out);
|
||||
}
|
||||
// return the associated user name and the auth-code type
|
||||
return [$token['user'], $data['type'] ?? "id"];
|
||||
}
|
||||
|
||||
protected function opTokenVerification(string $user, ServerRequestInterface $req): ResponseInterface {
|
||||
|
|
Loading…
Reference in a new issue