OAuth Flow

OAuth authorization code flow (PKCE) #

This is the flow for applications that access Microbooks on behalf of other users. Your app never sees the user’s password: it redirects them to Microbooks, the user signs in and approves the requested scopes, and your app receives tokens scoped to exactly what was granted.

Microbooks follows OAuth 2.1 practice: the authorization code grant, rotating refresh tokens, and no implicit flow. PKCE is required for public clients — the server rejects an authorization request from a client with no secret unless it carries a code_challenge. Confidential clients authenticate with their secret and are not forced to use PKCE, but should anyway.

Always send code_challenge_method=S256. If you omit the parameter the server falls back to plain, which puts the verifier in the query string of the authorization request and defeats the point. Only S256 is advertised in the server metadata.

Before you start you need a client — see Client registration.

Server metadata #

Endpoints and capabilities are published at the standard discovery location:

curl https://api.microbooks.io/.well-known/oauth-authorization-server

The endpoints referenced below:

EndpointURL
Authorizationhttps://api.microbooks.io/oauth/authorize
Tokenhttps://api.microbooks.io/oauth/token
Revocationhttps://api.microbooks.io/oauth/revoke
JWKShttps://api.microbooks.io/.well-known/jwks.json

1. Create a code verifier and challenge #

Generate a random code_verifier (43–128 characters, URL-safe), then derive the challenge:

code_challenge = base64url( sha256( code_verifier ) )
import base64, hashlib, secrets

verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(
    hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()

2. Redirect the user to authorize #

https://api.microbooks.io/oauth/authorize
    ?client_id=<your client id>
    &redirect_uri=<one of your registered redirect URIs>
    &response_type=code
    &scope=books:read%20books:write
    &state=<random anti-CSRF value>
    &code_challenge=<challenge>
    &code_challenge_method=S256

The user signs in (if needed) and reviews a consent screen listing the scopes you asked for. On approval Microbooks redirects back to your redirect_uri with ?code=...&state=....

Always verify state. Reject the callback if it does not exactly match the value you sent — this is your CSRF protection for the flow.

3. Exchange the code for tokens #

curl --request POST 'https://api.microbooks.io/oauth/token' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=<your client id>' \
--data-urlencode 'redirect_uri=<the same redirect URI>' \
--data-urlencode 'code=<code from the callback>' \
--data-urlencode 'code_verifier=<the verifier from step 1>'

Response #

{
    "token_type": "Bearer",
    "expires_in": 900,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
    "refresh_token": "def50200a1b2c3..."
}

Confidential clients (server-side apps holding a secret) also send client_secret; public clients (mobile, SPA, CLI) must not have one — PKCE is their proof.

4. Call the API #

curl 'https://api.microbooks.io/books/v1/entity' \
--header 'Authorization: Bearer <access_token>'

5. Refresh #

Access tokens live 15 minutes. Redeem the refresh token for a new pair:

curl --request POST 'https://api.microbooks.io/oauth/token' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'client_id=<your client id>' \
--data-urlencode 'refresh_token=<refresh_token>'
Refresh tokens rotate. Each refresh response contains a new refresh token and the old one is revoked. Persist the new one atomically — reusing a rotated-away token yields invalid_grant and the user has to authorize again.

Verifying tokens yourself #

Access tokens are RS256-signed JWTs. If you run your own resource server, verify signatures against the JWKS; the sub claim is the account id and scopes carries the granted scopes.

Tokens are issued without a kid header. The JWKS publishes a kid, but the tokens do not reference it, so a verifier cannot select the key by id. Try each published key and accept the first whose signature validates — treat only a signature failure as “wrong key”; expiry or malformed-token errors are fatal regardless of which key you tried.