Add IndieAuth token revocation endpoint (RFC 7009)

Implements the current IndieAuth spec's dedicated /revocation endpoint
so clients can self-revoke a token (e.g. on user sign-out), rather than
only supporting revocation via the admin side. Always responds 200 per
spec, whether the token was found or not, so callers can't use it to
probe token validity. Skips the legacy action=revoke-on-/token fallback
the spec mentions for older clients, since the only real client here is
already being updated to use the current endpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014625MfkGZ7GVdbqKme4a8L
This commit is contained in:
Jonny Barnes 2026-08-13 16:13:11 +01:00
commit 9c9a6392c8
Signed by: jonny
SSH key fingerprint: SHA256:CTuSlns5U7qlD9jqHvtnVmfYV3Zwl2Z7WnJ4/dqOaL8
5 changed files with 54 additions and 2 deletions

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\MicropubToken;
use App\Services\TokenService;
use GuzzleHttp\Psr7\Uri;
use Illuminate\Http\JsonResponse;
@ -24,6 +25,7 @@ class IndieAuthController extends Controller
'issuer' => config('app.url'),
'authorization_endpoint' => route('indieauth.start'),
'token_endpoint' => route('indieauth.token'),
'revocation_endpoint' => route('indieauth.revocation'),
'code_challenge_methods_supported' => ['S256'],
// 'introspection_endpoint' => route('indieauth.introspection'),
// 'introspection_endpoint_auth_methods_supported' => ['none'],
@ -178,6 +180,26 @@ class IndieAuthController extends Controller
]);
}
/**
* Process a POST request to the IndieAuth revocation endpoint (RFC 7009).
*
* Per spec this always returns HTTP 200, whether the token was revoked,
* unknown, or already revoked, so callers can't probe token validity.
*/
public function processRevocationRequest(Request $request): JsonResponse
{
$token = $request->get('token', '');
if ($token !== '') {
MicropubToken::where('token_hash', hash('sha256', $token))
->whereNull('revoked_at')
->first()
?->revoke();
}
return response()->json([], 200);
}
protected function isValidRedirectUri(string $clientId, string $redirectUri): bool
{
// If client_id is not a valid URL, then it's not valid

View file

@ -17,6 +17,7 @@ class LinkHeadersMiddleware
$response->header('Link', '<'.route('indieauth.metadata').'>; rel="indieauth-metadata"', false);
$response->header('Link', '<'.route('indieauth.start').'>; rel="authorization_endpoint"', false);
$response->header('Link', '<'.route('indieauth.token').'>; rel="token_endpoint"', false);
$response->header('Link', '<'.route('indieauth.revocation').'>; rel="revocation_endpoint"', false);
$response->header('Link', '<'.route('micropub-endpoint').'>; rel="micropub"', false);
$response->header('Link', '<'.route('webmention-endpoint').'>; rel="webmention"', false);