Documentation
Verifying access tokens
How your Micropub or Microsub endpoint checks the access tokens apps send it.
How it fits together
- Your home page links to your endpoints with
<link rel="micropub" href="…">and<link rel="microsub" href="…">. IndieKey reads them whenever it checks your website, and lists them under Resource servers. - An app signs you in and asks for scopes such as
create(Micropub) orread(Microsub). - IndieKey issues an access token for that endpoint: a JWT whose
audis the endpoint's URL. An app asking for both kinds of scope, like a Microsub reader that can also post replies, gets one token with both endpoints inaud. - The app sends requests to your endpoint with
Authorization: Bearer ACCESS_TOKEN, and your endpoint verifies the token.
Scopes defined for Micropub and Microsub go to the matching endpoint automatically. To accept other scopes, add them to an endpoint under Resource servers; scopes that are not routed anywhere are not granted. An app asking for Micropub or Microsub scopes when your home page does not link to that endpoint is refused.
Verifying the JWT
- Fetch the signing keys from
jwks_uriin your website's metadata (theindieauth-metadatalink on your home page), currentlyhttps://indiekey.id/jwks. Cache them, and fetch them again when a token names akidyou don't have. - Check the signature with the key matching the token's
kid. Tokens are signed withRS256, and their header'stypisat+jwt. - Check the claims:
issis your website's issuer,https://indiekey.id/s/SITE_ID/, shown under Resource servers and in your website's metadataaudis your endpoint's URL, or a list containing itexphas not passedme(also insub) is your website's URLscope, split on spaces, contains what the request needs
Access tokens last one hour unless you choose another lifetime for the endpoint; apps renew them with a refresh token. A JWT cannot be recalled once issued, so after you disconnect an app its token keeps working at an endpoint that only checks signatures until it expires. If that matters more than saving a request per token, use introspection.
Example in PHP
// composer require firebase/php-jwt
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
function verify_token(string $token, string $scope): ?array
{
$keys = json_decode(file_get_contents('https://indiekey.id/jwks'), true);
try {
$claims = (array) JWT::decode($token, JWK::parseKeySet($keys));
} catch (Exception $e) {
return null;
}
if ($claims['iss'] !== 'https://indiekey.id/s/SITE_ID/'
|| !in_array('https://example.com/micropub', (array) $claims['aud'], true)
|| $claims['me'] !== 'https://example.com/'
|| !in_array($scope, explode(' ', $claims['scope']), true)) {
return null;
}
return $claims;
}
Or ask this server
Instead of verifying JWTs, your endpoint can ask this server whether a token is active. Create an introspection credential for the endpoint under Resource servers and treat it like a password. It only sees tokens issued for that endpoint.
curl https://indiekey.id/introspect \
-H "Authorization: Bearer YOUR_CREDENTIAL" \
-d "token=ACCESS_TOKEN"
An active token:
{
"active": true,
"token_type": "Bearer",
"iss": "https://indiekey.id/s/SITE_ID/",
"sub": "https://example.com/",
"aud": "https://example.com/micropub",
"me": "https://example.com/",
"client_id": "https://app.example.com/",
"scope": "create media",
"iat": 1757750000,
"exp": 1757753600
}
Anything else (expired, revoked, unknown, or issued for a different endpoint) returns only { "active": false }. A missing or wrong credential gets HTTP 401. Check scope as above before doing the work.