Required cache-control headers on more responses
This commit is contained in:
parent
f66473cc53
commit
a0fe1b5f80
@ -29,8 +29,8 @@ interface AuthorizationFormInterface {
|
||||
* * `client_id`: the URL of the client app. Should be shown to the user. This also makes a good “cancel” link.
|
||||
* * `redirect_uri`: the URI which the user will be redirected to on successful authorization.
|
||||
*
|
||||
* The form MUST submit a POST request to $formAction, with the `taproot_indieauth_action`
|
||||
* set to `approve`.
|
||||
* The form MUST submit a POST request to `$formAction`, with the `taproot_indieauth_action`
|
||||
* parameter set to `approve`.
|
||||
*
|
||||
* The form MUST additionally include any CSRF tokens required to protect the submission.
|
||||
* Refer to whatever CSRF protection code you’re using (e.g. `\Taproot\IndieAuth\Middleware\DoubleSubmitCookieCsrfMiddleware`)
|
||||
@ -88,4 +88,4 @@ interface AuthorizationFormInterface {
|
||||
* @return array The $code data after making any necessary changes.
|
||||
*/
|
||||
public function transformAuthorizationCode(ServerRequestInterface $request, array $code): array;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,11 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
use function Taproot\IndieAuth\renderTemplate;
|
||||
|
||||
/**
|
||||
* Single User Password Authentication Callback
|
||||
*
|
||||
*
|
||||
*/
|
||||
class SingleUserPasswordAuthenticationCallback {
|
||||
const PASSWORD_FORM_PARAMETER = 'taproot_indieauth_server_password';
|
||||
|
||||
|
@ -11,7 +11,8 @@ use Dflydev\FigCookies;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use function Taproot\IndieAuth\generateRandomString;
|
||||
|
||||
use function Taproot\IndieAuth\generateRandomPrintableAsciiString;
|
||||
|
||||
/**
|
||||
* Double-Submit Cookie CSRF Middleware
|
||||
@ -50,6 +51,17 @@ class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface, LoggerAwa
|
||||
|
||||
public LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* The `$errorResponse` parameter can be used to customse the error response returned when a
|
||||
* write request has invalid CSRF parameters. It can take the following forms:
|
||||
*
|
||||
* * A `string`, which will be returned as-is with a 400 Status Code and `Content-type: text/plain` header
|
||||
* * An instance of `ResponseInterface`, which will be returned as-is
|
||||
* * A callable with the signature `function (ServerRequestInterface $request): ResponseInterface`,
|
||||
* the return value of which will be returned as-is.
|
||||
*/
|
||||
public function __construct(?string $attribute=self::ATTRIBUTE, ?int $ttl=self::TTL, $errorResponse=self::DEFAULT_ERROR_RESPONSE_STRING, $tokenLength=self::CSRF_TOKEN_LENGTH, $logger=null) {
|
||||
$this->attribute = $attribute ?? self::ATTRIBUTE;
|
||||
$this->ttl = $ttl ?? self::TTL;
|
||||
@ -78,7 +90,7 @@ class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface, LoggerAwa
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
|
||||
// Generate a new CSRF token, add it to the request attributes, and as a cookie on the response.
|
||||
$csrfToken = generateRandomString($this->tokenLength);
|
||||
$csrfToken = generateRandomPrintableAsciiString($this->tokenLength);
|
||||
$request = $request->withAttribute($this->attribute, $csrfToken);
|
||||
|
||||
if (!in_array(strtoupper($request->getMethod()), self::READ_METHODS) && !$this->isValid($request)) {
|
||||
|
@ -2,12 +2,13 @@
|
||||
|
||||
namespace Taproot\IndieAuth;
|
||||
|
||||
use BarnabyWalters\Mf2 as M;
|
||||
use Exception;
|
||||
use GuzzleHttp\Psr7\Header as HeaderParser;
|
||||
use IndieAuth\Client as IndieAuthClient;
|
||||
use Mf2;
|
||||
use BarnabyWalters\Mf2 as M;
|
||||
use GuzzleHttp\Psr7\Header as HeaderParser;
|
||||
use Nyholm\Psr7\Response;
|
||||
use PDO;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Http\Client\NetworkExceptionInterface;
|
||||
use Psr\Http\Client\RequestExceptionInterface;
|
||||
@ -18,28 +19,31 @@ use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Taproot\IndieAuth\Callback\AuthorizationFormInterface;
|
||||
use Taproot\IndieAuth\Callback\DefaultAuthorizationForm;
|
||||
|
||||
use Taproot\IndieAuth\Storage\TokenStorageInterface;
|
||||
|
||||
/**
|
||||
* IndieAuth Server
|
||||
*
|
||||
* A PSR-7 compatible implementation of the request-handling logic for IndieAuth authorization endpoints
|
||||
* A PSR-7-compatible implementation of the request-handling logic for IndieAuth authorization endpoints
|
||||
* and token endpoints.
|
||||
*
|
||||
* Typical usage looks something like this:
|
||||
* Typical minimal usage looks something like this:
|
||||
*
|
||||
* // Somewhere in your app set-up:
|
||||
*
|
||||
* use Taproot\IndieAuth;
|
||||
*
|
||||
* $server = new IndieAuth\Server([
|
||||
* 'secret' => APP_INDIEAUTH_SECRET,
|
||||
* 'tokenStorage' => '/../data/auth_tokens/',
|
||||
* 'handleAuthenticationRequestCallback' => new IndieAuth\Callback\SingleUserPasswordAuthenticationCallback([
|
||||
* 'me' => 'https://your-domain.com/'
|
||||
* ],
|
||||
* YOUR_HASHED_PASSWORD
|
||||
* )
|
||||
* // Somewhere in your app set-up code:
|
||||
* $server = new Taproot\IndieAuth\Server([
|
||||
* 'secret' => APP_INDIEAUTH_SECRET, // A secret key, >= 64 characters long.
|
||||
* 'tokenStorage' => '/../data/auth_tokens/', // A path to store token data, or an object implementing TokenStorageInterface.
|
||||
* 'handleAuthenticationRequestCallback' => function (ServerRequestInterface $request, string $authenticationRedirect, ?string $normalizedMeUrl) {
|
||||
* // If the request is authenticated, return an array with a `me` key containing the
|
||||
* // canonical URL of the currently logged-in user.
|
||||
* if ($userUrl = getLoggedInUserUrl($request)) {
|
||||
* return ['me' => $userUrl];
|
||||
* }
|
||||
*
|
||||
* // Otherwise, redirect the user to a login page, ensuring that they will be redirected
|
||||
* // back to the IndieAuth flow with query parameters intact once logged in.
|
||||
* return new Response('302', ['Location' => 'https://example.com/login?next=' . urlencode($authenticationRedirect)]);
|
||||
* }
|
||||
* ]);
|
||||
*
|
||||
* // In your authorization endpoint route:
|
||||
@ -47,6 +51,15 @@ use Taproot\IndieAuth\Callback\DefaultAuthorizationForm;
|
||||
*
|
||||
* // In your token endpoint route:
|
||||
* return $server->handleTokenEndpointRequest($request);
|
||||
*
|
||||
* // In another route (e.g. a micropub route), to authenticate the request:
|
||||
* // (assuming $bearerToken is a token parsed from an “Authorization: Bearer XXXXXX” header
|
||||
* // or access_token property from a request body)
|
||||
* if ($accessToken = $server->getTokenStorage()->getAccessToken($bearerToken)) {
|
||||
* // Request is authenticated as $accessToken['me'], and is allowed to
|
||||
* // act according to the scopes listed in $accessToken['scope'].
|
||||
* $scopes = explode(' ', $accessToken['scope']);
|
||||
* }
|
||||
*
|
||||
* Refer to the `__construct` documentation for further configuration options, and to the
|
||||
* documentation for both handling methods for further documentation about them.
|
||||
@ -120,8 +133,12 @@ class Server {
|
||||
* `$normalizedMeUrl`. Otherwise, this parameter is null. This parameter can optionally be used
|
||||
* as a suggestion for which user to log in as in a multi-user authentication flow, but should NOT
|
||||
* be considered valid data.
|
||||
*
|
||||
* If redirecting to an existing authentication flow, this callable can usually be implemented as a
|
||||
* closure. The callable may also implement its own authentication logic. For an example, see
|
||||
* `Callback\SingleUserPasswordAuthenticationCallback`.
|
||||
* * `secret`: A cryptographically random string with a minimum length of 64 characters. Used
|
||||
* to hash and subsequently query parameters which get passed around.
|
||||
* to hash and subsequently verify request query parameters which get passed around.
|
||||
* * `tokenStorage`: Either an object implementing `Storage\TokenStorageInterface`, or a string path,
|
||||
* which will be passed to `Storage\FilesystemJsonStorage`. This object handles persisting authorization
|
||||
* codes and access tokens, as well as implementation-specific parts of the exchange process which are
|
||||
@ -156,7 +173,7 @@ class Server {
|
||||
* made by your authentication or authorization pages, if it’s not convenient to put them elsewhere.
|
||||
* Returning `null` will result in a standard `invalid_request` error being returned.
|
||||
* * `logger`: An instance of `LoggerInterface`. Will be used for internal logging, and will also be set
|
||||
* as the logger for most objects passed in config which implement `LoggerAwareInterface`.
|
||||
* as the logger for any objects passed in config which implement `LoggerAwareInterface`.
|
||||
*
|
||||
* @param array $config An array of configuration variables
|
||||
* @return self
|
||||
@ -179,7 +196,7 @@ class Server {
|
||||
|
||||
$secret = $config['secret'] ?? '';
|
||||
if (!is_string($secret) || strlen($secret) < 64) {
|
||||
throw new Exception("\$config['secret'] must be a string with a minimum length of 64 characters. Make one with Taproot\IndieAuth\generateRandomString(64)");
|
||||
throw new Exception("\$config['secret'] must be a string with a minimum length of 64 characters.");
|
||||
}
|
||||
$this->secret = $secret;
|
||||
|
||||
@ -253,6 +270,10 @@ class Server {
|
||||
trySetLogger($this->authorizationForm, $this->logger);
|
||||
}
|
||||
|
||||
public function getTokenStorage(): TokenStorageInterface {
|
||||
return $this->tokenStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Authorization Endpoint Request
|
||||
*
|
||||
@ -357,7 +378,10 @@ class Server {
|
||||
// TODO: return an error if the token doesn’t contain a me key.
|
||||
|
||||
// If everything checked out, return {"me": "https://example.com"} response
|
||||
return new Response(200, ['content-type' => 'application/json'], json_encode(array_filter($token->getData(), function ($k) {
|
||||
return new Response(200, [
|
||||
'content-type' => 'application/json',
|
||||
'cache-control' => 'no-store',
|
||||
], json_encode(array_filter($token->getData(), function ($k) {
|
||||
return in_array($k, ['me', 'profile']);
|
||||
}, ARRAY_FILTER_USE_KEY)));
|
||||
}
|
||||
@ -501,10 +525,13 @@ class Server {
|
||||
}
|
||||
|
||||
// Return a redirect to the client app.
|
||||
return new Response(302, ['Location' => appendQueryParams($queryParams['redirect_uri'], [
|
||||
'code' => $authCode->getKey(),
|
||||
'state' => $code['state']
|
||||
])]);
|
||||
return new Response(302, [
|
||||
'Location' => appendQueryParams($queryParams['redirect_uri'], [
|
||||
'code' => $authCode->getKey(),
|
||||
'state' => $code['state']
|
||||
]),
|
||||
'Cache-control' => 'no-cache'
|
||||
]);
|
||||
}
|
||||
|
||||
// Otherwise, the user is authenticated and needs to authorize the client app + choose scopes.
|
||||
@ -566,7 +593,8 @@ class Server {
|
||||
}
|
||||
|
||||
// Present the authorization UI.
|
||||
return $this->authorizationForm->showForm($request, $authenticationResult, $authenticationRedirect, $clientHApp);
|
||||
return $this->authorizationForm->showForm($request, $authenticationResult, $authenticationRedirect, $clientHApp)
|
||||
->withAddedHeader('Cache-control', 'no-cache');
|
||||
}
|
||||
}
|
||||
|
||||
@ -670,7 +698,10 @@ class Server {
|
||||
}
|
||||
|
||||
// If everything checks out, generate an access token and return it.
|
||||
return new Response(200, ['content-type' => 'application/json'], json_encode(array_merge([
|
||||
return new Response(200, [
|
||||
'content-type' => 'application/json',
|
||||
'cache-control' => 'no-store'
|
||||
], json_encode(array_merge([
|
||||
'access_token' => $token->getKey(),
|
||||
'token_type' => 'Bearer'
|
||||
], array_filter($token->getData(), function ($k) {
|
||||
|
@ -25,6 +25,15 @@ function generateRandomString($numBytes) {
|
||||
return bin2hex($bytes);
|
||||
}
|
||||
|
||||
function generateRandomPrintableAsciiString(int $length) {
|
||||
$chars = [];
|
||||
while (count($chars) < $length) {
|
||||
// 0x21 to 0x7E is the entire printable ASCII range, not including space (0x20).
|
||||
$chars[] = chr(random_int(0x21, 0x7E));
|
||||
}
|
||||
return join('', $chars);
|
||||
}
|
||||
|
||||
function generatePKCECodeChallenge($plaintext) {
|
||||
return base64_urlencode(hash('sha256', $plaintext, true));
|
||||
}
|
||||
|
@ -352,7 +352,8 @@ EOT
|
||||
]);
|
||||
|
||||
$res = $s->handleAuthorizationEndpointRequest($req);
|
||||
|
||||
|
||||
$this->assertEquals('no-cache', $res->getHeaderLine('cache-control'));
|
||||
$this->assertEquals(200, $res->getStatusCode());
|
||||
}
|
||||
|
||||
@ -381,6 +382,7 @@ EOT
|
||||
|
||||
$res = $s->handleAuthorizationEndpointRequest($req);
|
||||
|
||||
$this->assertEquals('no-cache', $res->getHeaderLine('cache-control'));
|
||||
$this->assertEquals(200, $res->getStatusCode());
|
||||
}
|
||||
|
||||
@ -406,6 +408,7 @@ EOT
|
||||
|
||||
$res = $s->handleAuthorizationEndpointRequest($req);
|
||||
|
||||
$this->assertEquals('no-cache', $res->getHeaderLine('cache-control'));
|
||||
$this->assertEquals(200, $res->getStatusCode());
|
||||
}
|
||||
|
||||
@ -508,6 +511,7 @@ EOT
|
||||
|
||||
$res = $s->handleAuthorizationEndpointRequest($req);
|
||||
|
||||
$this->assertEquals('no-cache', $res->getHeaderLine('cache-control'));
|
||||
$this->assertEquals(302, $res->getStatusCode(), 'The Response from a successful approval request must be a 302 redirect.');
|
||||
|
||||
$responseLocation = $res->getHeaderLine('location');
|
||||
@ -686,6 +690,7 @@ EOT
|
||||
$res = $s->handleAuthorizationEndpointRequest($req);
|
||||
|
||||
$this->assertEquals(200, $res->getStatusCode());
|
||||
$this->assertEquals('no-store', $res->getHeaderLine('cache-control'));
|
||||
$resJson = json_decode((string) $res->getBody(), true);
|
||||
$this->assertEquals([
|
||||
'me' => 'https://me.example.com/',
|
||||
@ -772,6 +777,7 @@ EOT
|
||||
$res = $s->handleTokenEndpointRequest($req);
|
||||
|
||||
$this->assertEquals(200, $res->getStatusCode());
|
||||
$this->assertEquals('no-store', $res->getHeaderLine('cache-control'));
|
||||
$resJson = json_decode((string) $res->getBody(), true);
|
||||
$this->assertEquals(hash_hmac('sha256', $authCode->getKey(), SERVER_SECRET), $resJson['access_token']);
|
||||
$this->assertEquals('Bearer', $resJson['token_type']);
|
||||
|
Reference in New Issue
Block a user