Revoking tokens¶
POST /oauth/revoke_token/ implements RFC 7009
token revocation. Use it to give up an access or refresh token your app no
longer needs, and — this is the important case — to force a token out of
service immediately after a credential compromise, instead of waiting for it
to expire on its own.
Request¶
POST /oauth/revoke_token/
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
token=<access_or_refresh_token>
token_type_hint=access_token|refresh_token (optional)
Auth is the same HTTP Basic client_id:client_secret as
/oauth/token/.
A public client (no secret) sends client_id in the body instead, exactly as
it does at the token endpoint.
token_type_hint is an optimisation, not a requirement — omit it and the
server tries both token types. Set it when you know which kind you're
revoking; it saves a lookup.
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$RT" \
-d token_type_hint=refresh_token \
https://api.lucibook.co.uk/oauth/revoke_token/
import requests
resp = requests.post(
"https://api.lucibook.co.uk/oauth/revoke_token/",
auth=(client_id, client_secret),
data={"token": refresh_token, "token_type_hint": "refresh_token"},
timeout=10,
)
Response¶
200 on success — including when the token you sent was already
invalid, expired, or unknown. This is RFC 7009 §2.2, not a LuciBook
peculiarity: the endpoint's job is to guarantee the token is not usable
afterwards, and a token that was never usable in the first place already
satisfies that. Don't treat a 200 as proof the token existed; treat a
non-200 as your own request being malformed (bad client auth, missing
token).
What gets revoked¶
- Revoking a refresh token revokes it, the access token it is currently paired with, and every active connection it authorised (see below). It does not reach back and revoke earlier refresh tokens from the same rotation lineage — each refresh already invalidated the one before it (see Refresh-token rotation semantics), so by the time you hold the current refresh token there is only one live one to revoke.
- Revoking an access token revokes only that access token — no connection is touched. Its refresh token, if any, is untouched and can still be used to mint a new access token — call revoke again with that refresh token if you want the whole pair, and the connection(s) it authorised, gone.
Revoking a refresh token also revokes the connection(s)¶
Unlike an access-token revoke, revoking a refresh token revokes every
active connection that (your app, that user)
holds. A bearer token here is scoped to (application, user) only — it
carries no single org — so the same refresh token is valid against every
org that user has connected your app to, and revoking it closes all of
them, not just one. Concretely: revoking ONE refresh token revokes ALL of
that user's active connections for your app across EVERY org, including
connections currently backed by a completely DIFFERENT, still-live token
chain of the same (your app, that user) pair — a second device or session
that separately authorized the same user against the same app is not
exempt. Each connection is stamped revoked_at, revoked_reason is
recorded, and connection.revoked fires for every one — identical to what
DELETE /api/v1/connections/{connection_id}
does for a single connection. This closes the gap where revoking a token
alone (the old behaviour) left the connection live, so a leaked client
secret could revoke every token it had minted and then silently re-mint
new ones against that same still-active connection via a fresh
authorization-code or refresh exchange.
Only a token your app actually owns can trigger this: the endpoint checks
the submitted token's application against the client authenticated on
this request (its own secret, or its own client_id for a public
client) and does nothing — no revoke, no cascade, still 200 — for a
token that belongs to a different application. A leaked secret lets an
attacker revoke tokens/connections for your own app's users, which
rotating the secret (My Apps → app settings) stops going forward; it
never lets them reach another app's tokens or connections through this
endpoint.
Resubmitting the same token does not re-trigger the cascade. The
cascade only fires on the call that actually moves a refresh token from
live to revoked — a second POST with the same (now already-revoked)
token string is still a 200, but it never cascades again, even from
the owning client. This matters if a cascade fails partway through: one
of this user's connections in a rare case (a database error revoking
that one row) can be left live while everything else the token
authorised is gone. Retrying the revoke call will not finish the job
— the token is already revoked, so there is nothing left for a retry to
key off. If you have reason to believe a cascade didn't fully complete
(check for connection.revoked events you expected but didn't receive),
revoke the specific remaining connection(s) directly instead:
DELETE /api/v1/connections/{connection_id}
Authorization: Bearer <access token>
That call needs a still-valid access token. If you no longer have one, ask the organisation's user to remove your app under Settings → Connected apps, which revokes the connection the same way.
— note this needs a still-live bearer token to authenticate, so do it with a different token/connection you still hold, or ask the user to reconnect and then clean up via Connections.
Revoking an access token does not touch any connection — same as before. It revokes only that access token; its refresh token, if any, can still mint a new access token, and the connection(s) it is scoped to stay active until you revoke the refresh token or the connection(s) directly:
DELETE /api/v1/connections/{connection_id}
Authorization: Bearer <access token>
(see Connections). That
call also stamps revoked_at and fires connection.revoked — no read
scope is required to subscribe to it, since a connection-scoped event only
ever reaches an app that already held the connection. Rotating your client
secret (My Apps → app settings) is the separate step for "this app's
credential itself was compromised" — token/connection revocation does not
touch client_secret.
There is no ordering hazard between the two calls any more: revoking the
refresh token now revokes its connection(s) as part of the same call, so
you don't need to call DELETE /api/v1/connections/{connection_id}
separately just to make sure the connection dies too. The DELETE call
still exists for the narrower case where you want to revoke ONE specific
connection without touching the user's other tokens or connections for
your app.
After revoking¶
Once you've decided the user needs to reconnect — token revoked, connection revoked, or both — send them back through the authorize flow with a fresh consent: see Register an app and Authorization Code + PKCE. There is no "resume" affordance; a revoked connection is re-established the same way the first one was created.