Documentation

Verifying access tokens

How your Micropub or Microsub endpoint checks the access tokens apps send it.

How it fits together

  1. 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.
  2. An app signs you in and asks for scopes such as create (Micropub) or read (Microsub).
  3. IndieKey issues an access token for that endpoint: a JWT whose aud is 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 in aud.
  4. 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

  1. Fetch the signing keys from jwks_uri in your website's metadata (the indieauth-metadata link on your home page), currently https://dev.indiekey.id/jwks. Cache them, and fetch them again when a token names a kid you don't have.
  2. Check the signature with the key matching the token's kid. Tokens are signed with RS256, and their header's typ is at+jwt.
  3. Check the claims:
    • iss is your website's issuer, https://dev.indiekey.id/s/SITE_ID/, shown under Resource servers and in your website's metadata
    • aud is your endpoint's URL, or a list containing it
    • exp has not passed
    • me (also in sub) is your website's URL
    • scope, 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://dev.indiekey.id/jwks'), true);

    try {
        $claims = (array) JWT::decode($token, JWK::parseKeySet($keys));
    } catch (Exception $e) {
        return null;
    }

    if ($claims['iss'] !== 'https://dev.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://dev.indiekey.id/introspect \
  -H "Authorization: Bearer YOUR_CREDENTIAL" \
  -d "token=ACCESS_TOKEN"

An active token:

{
  "active": true,
  "token_type": "Bearer",
  "iss": "https://dev.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.