Initial commit
Sketched out first draft of how the library should work, stubbed a lot of the smaller utility classes required, and outlined the main handler functions for the IA Server.
This commit is contained in:
commit
8ab57bee25
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
vendor
|
24
composer.json
Normal file
24
composer.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "taproot/indieauth",
|
||||||
|
"description": "PHP PSR-7-compliant IndieAuth Server and Client implementation.",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Barnaby Walters",
|
||||||
|
"email": "barnaby@waterpigs.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"psr/http-message": "^1.0",
|
||||||
|
"psr/log": "^1.1",
|
||||||
|
"nyholm/psr7": "^1.4",
|
||||||
|
"indieauth/client": "^1.1",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"psr/http-server-middleware": "^1.0",
|
||||||
|
"dflydev/fig-cookies": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"guzzlehttp/guzzle": "^7.3"
|
||||||
|
}
|
||||||
|
}
|
1067
composer.lock
generated
Normal file
1067
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
src/Taproot/IndieAuth/ClosureRequestHandler.php
Normal file
22
src/Taproot/IndieAuth/ClosureRequestHandler.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Taproot\IndieAuth;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
|
class ClosureRequestHandler implements RequestHandlerInterface {
|
||||||
|
protected callable $callable;
|
||||||
|
|
||||||
|
protected array $args;
|
||||||
|
|
||||||
|
public function __construct(callable $callable) {
|
||||||
|
$this->callable = $callable;
|
||||||
|
$this->args = array_slice(func_get_args(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(ServerRequestInterface $request): ResponseInterface {
|
||||||
|
return call_user_func_array($this->callable, array_merge([$request], $this->args));
|
||||||
|
}
|
||||||
|
}
|
92
src/Taproot/IndieAuth/DoubleSubmitCookieCsrfMiddleware.php
Normal file
92
src/Taproot/IndieAuth/DoubleSubmitCookieCsrfMiddleware.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Taproot\IndieAuth;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Dflydev\FigCookies;
|
||||||
|
|
||||||
|
// From https://github.com/indieweb/indieauth-client-php/blob/main/src/IndieAuth/Client.php, thanks aaronpk.
|
||||||
|
function generateRandomString($numBytes) {
|
||||||
|
if (function_exists('random_bytes')) {
|
||||||
|
$bytes = random_bytes($numBytes);
|
||||||
|
} elseif (function_exists('openssl_random_pseudo_bytes')){
|
||||||
|
$bytes = openssl_random_pseudo_bytes($numBytes);
|
||||||
|
} else {
|
||||||
|
$bytes = '';
|
||||||
|
for($i=0, $bytes=''; $i < $numBytes; $i++) {
|
||||||
|
$bytes .= chr(mt_rand(0, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bin2hex($bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface {
|
||||||
|
const READ_METHODS = ['HEAD', 'GET', 'OPTIONS'];
|
||||||
|
const TTL = 60 * 20;
|
||||||
|
const ATTRIBUTE = 'csrf';
|
||||||
|
const DEFAULT_ERROR_RESPONSE_STRING = 'Invalid or missing CSRF token!';
|
||||||
|
const CSRF_TOKEN_LENGTH = 128;
|
||||||
|
|
||||||
|
public string $attribute;
|
||||||
|
|
||||||
|
public int $ttl;
|
||||||
|
|
||||||
|
public callable $errorResponse;
|
||||||
|
|
||||||
|
public int $tokenLength;
|
||||||
|
|
||||||
|
public function __construct(string $attribute=self::ATTRIBUTE, int $ttl=self::TTL, $errorResponse=self::DEFAULT_ERROR_RESPONSE_STRING, $tokenLength=self::CSRF_TOKEN_LENGTH) {
|
||||||
|
$this->attribute = $attribute;
|
||||||
|
$this->ttl = $ttl;
|
||||||
|
$this->tokenLength = $tokenLength;
|
||||||
|
|
||||||
|
if (!is_callable($errorResponse)) {
|
||||||
|
if (!$errorResponse instanceof ResponseInterface) {
|
||||||
|
if (!is_string($errorResponse)) {
|
||||||
|
$errorResponse = self::DEFAULT_ERROR_RESPONSE_STRING;
|
||||||
|
}
|
||||||
|
$errorResponse = new Response(400, ['content-type' => 'text/plain'], $errorResponse);
|
||||||
|
}
|
||||||
|
$errorResponse = function (ServerRequestInterface $request) use ($errorResponse) { return $errorResponse; };
|
||||||
|
}
|
||||||
|
$this->errorResponse = $errorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
|
||||||
|
if (!in_array(strtoupper($request->getMethod()), self::READ_METHODS)) {
|
||||||
|
// This request is a write method and requires CSRF protection.
|
||||||
|
if (!$this->isValid($request)) {
|
||||||
|
return call_user_func($this->errorResponse, $request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, generate a new CSRF token, add it to the request attributes, and as a cookie on the response.
|
||||||
|
$csrfToken = generateRandomString($this->tokenLength);
|
||||||
|
$request = $request->withAttribute($this->attribute, $csrfToken);
|
||||||
|
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
// Add the new CSRF cookie, restricting its scope to match the current request.
|
||||||
|
$response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create($this->attribute)
|
||||||
|
->withValue($csrfToken)
|
||||||
|
->withMaxAge($this->ttl)
|
||||||
|
->withSecure($request->getUri()->getScheme() == 'https')
|
||||||
|
->withDomain($request->getUri()->getHost())
|
||||||
|
->withPath($request->getUri()->getPath()));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isValid(ServerRequestInterface $request) {
|
||||||
|
if (in_array($this->attribute, $request->getParsedBody())) {
|
||||||
|
if (in_array($this->attribute, $request->getCookieParams())) {
|
||||||
|
return hash_equals($request->getParsedBody()[$this->attribute], $request->getCookieParams()[$this->attribute]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
68
src/Taproot/IndieAuth/FilesystemJsonStorage.php
Normal file
68
src/Taproot/IndieAuth/FilesystemJsonStorage.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Taproot\IndieAuth;
|
||||||
|
|
||||||
|
use DirectoryIterator;
|
||||||
|
|
||||||
|
class FilesystemJsonStorage implements TokenStorageInterface {
|
||||||
|
protected $path;
|
||||||
|
protected $ttl;
|
||||||
|
|
||||||
|
public function __construct(string $path, $ttl=0, $cleanUpNow=false) {
|
||||||
|
$this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||||
|
$this->ttl = $ttl;
|
||||||
|
|
||||||
|
if ($cleanUpNow) {
|
||||||
|
$this->cleanUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cleanUp($ttl=null): int {
|
||||||
|
$ttl = $ttl ?? $this->ttl;
|
||||||
|
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
// A TTL of 0 means the token should live until deleted, and negative TTLs are invalid.
|
||||||
|
if ($ttl >= 0) {
|
||||||
|
foreach (new DirectoryIterator($this->path) as $fileInfo) {
|
||||||
|
if ($fileInfo->isFile() && time() - max($fileInfo->getMTime(), $fileInfo->getCTime()) > $ttl) {
|
||||||
|
unlink($fileInfo->getPathname());
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key): ?array {
|
||||||
|
$path = $this->getPath($key);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$result = json_decode(file_get_contents($path), true);
|
||||||
|
|
||||||
|
if (is_array($result)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $key, array $data): bool {
|
||||||
|
// Ensure that the containing folder exists.
|
||||||
|
mkdir($this->path, 0777, true);
|
||||||
|
|
||||||
|
return file_put_contents($this->getPath($key), json_encode($data)) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $key): bool {
|
||||||
|
if (file_exists($this->getPath($key))) {
|
||||||
|
return unlink($this->getPath($key));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(string $key): string {
|
||||||
|
return $this->path . "$key.json";
|
||||||
|
}
|
||||||
|
}
|
14
src/Taproot/IndieAuth/NoOpMiddleware.php
Normal file
14
src/Taproot/IndieAuth/NoOpMiddleware.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Taproot\IndieAuth;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
|
class NoOpMiddleware implements MiddlewareInterface {
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
206
src/Taproot/IndieAuth/Server.php
Normal file
206
src/Taproot/IndieAuth/Server.php
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Taproot\IndieAuth;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
use GuzzleHttp\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use IndieAuth\Client as IndieAuthClient;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Development Reference
|
||||||
|
*
|
||||||
|
* Specification: https://indieauth.spec.indieweb.org/
|
||||||
|
* Error responses: https://www.rfc-editor.org/rfc/rfc6749.html#section-5.2
|
||||||
|
* indieweb/indieauth-client: https://github.com/indieweb/indieauth-client-php
|
||||||
|
* CSRF protection cheat sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
|
||||||
|
* Example CSRF protection cookie middleware: https://github.com/zakirullin/csrf-middleware/blob/master/src/CSRF.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: maybe move these to a functions file so they’re usable by consumers even when the class isn’t loaded.
|
||||||
|
// Alternatively, make them static methods so they can be autoloaded.
|
||||||
|
function isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) {
|
||||||
|
return strtolower($request->getMethod()) == 'post'
|
||||||
|
&& array_key_exists('grant_type', $request->getParsedBody())
|
||||||
|
&& $request->getParsedBody()['grant_type'] == 'authorization_code';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIndieAuthAuthorizationRequest(ServerRequestInterface $request, $permittedMethods=['get']) {
|
||||||
|
return in_array(strtolower($request->getMethod()), array_map('strtolower', $permittedMethods))
|
||||||
|
&& array_key_exists('response_type', $request->getQueryParams())
|
||||||
|
&& $request->getQueryParams()['response_type'] == 'code';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthorizationApprovalRequest(ServerRequestInterface $request) {
|
||||||
|
return strtolower($request->getMethod()) == 'post'
|
||||||
|
&& array_key_exists('taproot_indieauth_action', $request->getParsedBody())
|
||||||
|
&& $request->getParsedBody()['taproot_indieauth_action'] == 'approve';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueryString(array $parameters) {
|
||||||
|
$qs = [];
|
||||||
|
foreach ($parameters as $k => $v) {
|
||||||
|
$qs[] = urlencode($k) . '=' . urlencode($v);
|
||||||
|
}
|
||||||
|
return join('&', $qs);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
const CUSTOMIZE_AUTHORIZATION_CODE = 'customise_authorization_code';
|
||||||
|
const SHOW_AUTHORIZATION_PAGE = 'show_authorization_page';
|
||||||
|
const HANDLE_NON_INDIEAUTH_REQUEST = 'handle_non_indieauth_request';
|
||||||
|
const HANDLE_AUTHENTICATION_REQUEST = 'handle_authentication_request';
|
||||||
|
const DEFAULT_CSRF_KEY = 'taproot_indieauth_server_csrf';
|
||||||
|
|
||||||
|
public $callbacks;
|
||||||
|
|
||||||
|
public TokenStorageInterface $authorizationCodeStorage;
|
||||||
|
|
||||||
|
public TokenStorageInterface $accessTokenStorage;
|
||||||
|
|
||||||
|
public MiddlewareInterface $csrfMiddleware;
|
||||||
|
|
||||||
|
public LoggerInterface $logger;
|
||||||
|
|
||||||
|
public string $csrfKey;
|
||||||
|
|
||||||
|
public function __construct(array $callbacks, $authorizationCodeStorage, $accessTokenStorage, $csrfMiddleware=null, string $csrfKey=self::DEFAULT_CSRF_KEY, LoggerInterface $logger=null) {
|
||||||
|
$callbacks = array_merge([
|
||||||
|
self::CUSTOMIZE_AUTHORIZATION_CODE => function (array $code) { return $code; }, // Default to no-op.
|
||||||
|
self::SHOW_AUTHORIZATION_PAGE => function (ServerRequestInterface $request, array $authenticationResult, string $authenticationRedirect) { }, // TODO: Put the default implementation here.
|
||||||
|
self::HANDLE_NON_INDIEAUTH_REQUEST => function (ServerRequestInterface $request) { return null; }, // Default to no-op.
|
||||||
|
], $callbacks);
|
||||||
|
|
||||||
|
if (!(array_key_exists(self::HANDLE_AUTHENTICATION_REQUEST, $callbacks) and is_callable($callbacks[self::HANDLE_AUTHENTICATION_REQUEST]))) {
|
||||||
|
throw new Exception('$callbacks[\'' . self::HANDLE_AUTHENTICATION_REQUEST .'\'] must be present and callable.');
|
||||||
|
}
|
||||||
|
$this->callbacks = $callbacks;
|
||||||
|
|
||||||
|
if (!$authorizationCodeStorage instanceof TokenStorageInterface) {
|
||||||
|
if (is_string($authorizationCodeStorage)) {
|
||||||
|
$authorizationCodeStorage = new FilesystemJsonStorage($authorizationCodeStorage, 600, true);
|
||||||
|
} else {
|
||||||
|
throw new Exception('$authorizationCodeStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->authorizationCodeStorage = $authorizationCodeStorage;
|
||||||
|
|
||||||
|
if (!$accessTokenStorage instanceof TokenStorageInterface) {
|
||||||
|
if (is_string($accessTokenStorage)) {
|
||||||
|
// Create a default access token storage with a TTL of 7 days.
|
||||||
|
$accessTokenStorage = new FilesystemJsonStorage($accessTokenStorage, 60 * 60 * 24 * 7, true);
|
||||||
|
} else {
|
||||||
|
throw new Exception('$accessTokenStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->accessTokenStorage = $accessTokenStorage;
|
||||||
|
|
||||||
|
$this->csrfKey = $csrfKey;
|
||||||
|
|
||||||
|
if (!$csrfMiddleware instanceof MiddlewareInterface) {
|
||||||
|
// Default to the statless Double-Submit Cookie CSRF Middleware, with default settings.
|
||||||
|
$csrfMiddleware = new DoubleSubmitCookieCsrfMiddleware($this->csrfKey);
|
||||||
|
}
|
||||||
|
$this->csrfMiddleware = $csrfMiddleware;
|
||||||
|
|
||||||
|
$this->logger = $logger ?? new NullLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleAuthorizationEndpointRequest(ServerRequestInterface $request): ResponseInterface {
|
||||||
|
// If it’s a profile information request:
|
||||||
|
if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) {
|
||||||
|
// Verify that the authorization code is valid and has not yet been used.
|
||||||
|
$this->authorizationCodeStorage->get($request->getParsedBody()['code']);
|
||||||
|
|
||||||
|
// Verify that it was issued for the same client_id and redirect_uri
|
||||||
|
|
||||||
|
// Check that the supplied code_verifier hashes to the stored code_challenge
|
||||||
|
|
||||||
|
// If everything checked out, return {"me": "https://example.com"} response
|
||||||
|
// (a response containing any additional information must contain a valid scope value, and
|
||||||
|
// be handled by the token_endpoint).
|
||||||
|
// TODO: according to the spec, it is technically permitted for the authorization endpoint
|
||||||
|
// to additional provide profile information. Leave it up to the library consumer to decide
|
||||||
|
// whether to add it or not.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because the special case above isn’t allowed to be CSRF-protected, we have to do some rather silly
|
||||||
|
// gymnastics here to selectively-CSRF-protect requests which do need it.
|
||||||
|
return $this->csrfMiddleware->process($request, new ClosureRequestHandler(function (ServerRequestInterface $request) {
|
||||||
|
// If this is an authorization or approval request (allowing POST requests as well to accommodate
|
||||||
|
// approval requests and custom auth form submission.
|
||||||
|
if (isIndieAuthAuthorizationRequest($request, ['get', 'post'])) {
|
||||||
|
// Build a URL for the authentication flow to redirect to, if it needs to.
|
||||||
|
// TODO: perhaps filter queryParams to only include the indieauth-relevant params?
|
||||||
|
$authenticationRedirect = $request->getUri() . '?' . buildQueryString($request->getQueryParams());
|
||||||
|
|
||||||
|
$authenticationResult = call_user_func($this->callbacks[self::HANDLE_AUTHENTICATION_REQUEST], $request, $authenticationRedirect);
|
||||||
|
|
||||||
|
// If the authentication handler returned a Response, return that as-is.
|
||||||
|
if ($authenticationResult instanceof Response) {
|
||||||
|
return $authenticationResult;
|
||||||
|
} elseif (is_array($authenticationResult)) {
|
||||||
|
// Check the resulting array for errors.
|
||||||
|
|
||||||
|
// The user is logged in.
|
||||||
|
|
||||||
|
// If this is a POST request sent from the authorization (i.e. scope-choosing) form:
|
||||||
|
if (isAuthorizationApprovalRequest($request)) {
|
||||||
|
// Assemble the data for the authorization code, store it somewhere persistent.
|
||||||
|
$code = [
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
// Pass it to the auth code customisation callback, if any.
|
||||||
|
$code = call_user_func($this->callbacks[self::CUSTOMIZE_AUTHORIZATION_CODE], $code, $request);
|
||||||
|
|
||||||
|
// Store the authorization code.
|
||||||
|
$this->authorizationCodeStorage->put($code['code'], $code);
|
||||||
|
|
||||||
|
// Return a redirect to the client app.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, the user is authenticated and needs to authorize the client app + choose scopes.
|
||||||
|
|
||||||
|
// Fetch the client_id URL to find information about the client to present to the user.
|
||||||
|
|
||||||
|
// If the authority of the redirect_uri does not match the client_id or one of their redirect URLs, return an error.
|
||||||
|
|
||||||
|
// Present the authorization UI.
|
||||||
|
return call_user_func($this->callbacks[self::SHOW_AUTHORIZATION_PAGE], $request, $authenticationResult, $authenticationRedirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request isn’t an IndieAuth Authorization or Code-redeeming request, it’s either an invalid
|
||||||
|
// request or something to do with a custom auth handler (e.g. sending a one-time code in an email.)
|
||||||
|
$nonIndieAuthRequestResult = call_user_func($this->callbacks[self::HANDLE_NON_INDIEAUTH_REQUEST], $request);
|
||||||
|
if ($nonIndieAuthRequestResult instanceof ResponseInterface) {
|
||||||
|
return $nonIndieAuthRequestResult;
|
||||||
|
} else {
|
||||||
|
return new Response(400, ['content-type' => 'application/json'], json_encode([
|
||||||
|
'error' => 'invalid_request'
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleTokenEndpointRequest(ServerRequestInterface $request): ResponseInterface {
|
||||||
|
// This is a request to redeem an authorization_code for an access_token.
|
||||||
|
|
||||||
|
// Verify that the authorization code is valid and has not yet been used.
|
||||||
|
|
||||||
|
// Verify that it was issued for the same client_id and redirect_uri
|
||||||
|
|
||||||
|
// Check that the supplied code_verifier hashes to the stored code_challenge
|
||||||
|
|
||||||
|
// If the auth code was issued with no scope, return an error.
|
||||||
|
|
||||||
|
// If everything checks out, generate an access token and return it.
|
||||||
|
}
|
||||||
|
}
|
15
src/Taproot/IndieAuth/TokenStorageInterface.php
Normal file
15
src/Taproot/IndieAuth/TokenStorageInterface.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Taproot\IndieAuth;
|
||||||
|
|
||||||
|
// TODO: document.
|
||||||
|
|
||||||
|
interface TokenStorageInterface {
|
||||||
|
public function cleanUp($ttl=null): int;
|
||||||
|
|
||||||
|
public function get(string $key): ?array;
|
||||||
|
|
||||||
|
public function put(string $key, array $data): bool;
|
||||||
|
|
||||||
|
public function delete(string $key): bool;
|
||||||
|
}
|
Reference in New Issue
Block a user