Add IndieAuth token introspection endpoint (RFC 7662)
Lets a resource server (or a client checking its own token, via
self-introspection) verify a token's active/me/client_id/scope without
needing to be tightly coupled to this token endpoint. Requires the
caller to present their own currently-active token as authorization,
per spec's requirement that the endpoint MUST require some form of
authorization. Inactive tokens get back only {"active": false}, no
detail on why, matching the privacy stance already used for
revocation.
Pulled the hash-and-lookup-active-token logic (now needed a third
time) into MicropubToken::findActive(), used by this, the revocation
endpoint, and VerifyMicropubToken.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014625MfkGZ7GVdbqKme4a8L
This commit is contained in:
parent
9d6cf6c815
commit
24da24a677
5 changed files with 125 additions and 12 deletions
|
|
@ -719,4 +719,80 @@ class IndieAuthTest extends TestCase
|
|||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function introspection_requires_a_bearer_token(): void
|
||||
{
|
||||
$response = $this->post('/introspect', ['token' => 'irrelevant']);
|
||||
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function introspection_rejects_a_revoked_bearer_token(): void
|
||||
{
|
||||
$callerToken = resolve(TokenService::class)->getNewToken([
|
||||
'me' => config('app.url'),
|
||||
'client_id' => 'https://app.example.com',
|
||||
'scope' => 'create',
|
||||
]);
|
||||
MicropubToken::where('token_hash', hash('sha256', $callerToken))->firstOrFail()->revoke();
|
||||
|
||||
$response = $this->post(
|
||||
'/introspect',
|
||||
['token' => 'irrelevant'],
|
||||
['HTTP_Authorization' => 'Bearer '.$callerToken]
|
||||
);
|
||||
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function introspection_returns_active_details_for_a_valid_token(): void
|
||||
{
|
||||
$callerToken = resolve(TokenService::class)->getNewToken([
|
||||
'me' => config('app.url'),
|
||||
'client_id' => 'https://app.example.com',
|
||||
'scope' => 'create',
|
||||
]);
|
||||
$subjectToken = resolve(TokenService::class)->getNewToken([
|
||||
'me' => 'https://someone-else.example.com',
|
||||
'client_id' => 'https://quill.p3k.io',
|
||||
'scope' => 'create update',
|
||||
]);
|
||||
|
||||
$response = $this->post(
|
||||
'/introspect',
|
||||
['token' => $subjectToken],
|
||||
['HTTP_Authorization' => 'Bearer '.$callerToken]
|
||||
);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson([
|
||||
'active' => true,
|
||||
'me' => 'https://someone-else.example.com',
|
||||
'client_id' => 'https://quill.p3k.io',
|
||||
'scope' => 'create update',
|
||||
]);
|
||||
$response->assertJsonStructure(['iat']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function introspection_returns_only_active_false_for_an_unknown_token(): void
|
||||
{
|
||||
$callerToken = resolve(TokenService::class)->getNewToken([
|
||||
'me' => config('app.url'),
|
||||
'client_id' => 'https://app.example.com',
|
||||
'scope' => 'create',
|
||||
]);
|
||||
|
||||
$response = $this->post(
|
||||
'/introspect',
|
||||
['token' => bin2hex(random_bytes(32))],
|
||||
['HTTP_Authorization' => 'Bearer '.$callerToken]
|
||||
);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertExactJson(['active' => false]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue