Introspecting tokens¶
POST /oauth/introspect/ implements RFC 7662
token introspection. Use it to check, server-side, whether an access token
you're holding is still valid, and to read back its scope and expiry —
without waiting for a 401 from a resource endpoint to find out.
Client auth¶
Authenticate as your app, the same way as /oauth/token/
and /oauth/revoke_token/. HTTP Basic is preferred:
Authorization: Basic base64(client_id:client_secret)
Body credentials are a supported fallback (RFC 6749 §2.3.1 shape, "NOT RECOMMENDED" but accepted) for clients that can't send a Basic header:
client_id=<client_id>&client_secret=<client_secret>
alongside token= in the same form-encoded body. A request that fails
both — no valid Basic header and no valid body credentials — never reaches
token lookup at all; the endpoint answers before that.
Request¶
POST /oauth/introspect/
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
token=<access_token>
GET /oauth/introspect/?token=<access_token> (with the same Authorization
header) works identically — both methods are gated the same way.
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$ACCESS_TOKEN" \
https://api.lucibook.co.uk/oauth/introspect/
import requests
resp = requests.post(
"https://api.lucibook.co.uk/oauth/introspect/",
auth=(client_id, client_secret),
data={"token": access_token},
timeout=10,
)
Response¶
A successfully authenticated call always gets 200, with a JSON body.
The only field every one of those responses carries is active:
{"active": true, "scope": "bills:read contacts:read", "exp": 1790186013, "client_id": "..."}
When active is false, that is the entire body — no other keys:
{"active": false}
A call that fails client authentication — bad or missing Basic/body
credentials — never reaches token lookup and gets a plain 403 with no
body. RFC 7662 §2.3 specifies 401 for invalid client credentials;
LuciBook currently returns 403, so handle both as "check your client
credentials".
Fields on an active response — django-oauth-toolkit's
IntrospectTokenView.get_token_response (oauth2_provider/views/
introspect.py), with username removed by LuciBook (see
below):
| Field | Always present? | Meaning |
|---|---|---|
active |
yes | true |
scope |
yes | space-separated scope string on the token |
exp |
yes | Unix timestamp the token expires at |
client_id |
yes | your app's client_id |
There is no token_type, sub, or iat field — this endpoint returns
exactly the fields above, nothing more.
A refresh-token string passed as token= isn't recognised by this
endpoint at all (django-oauth-toolkit's introspection view only ever looks
up access tokens) and gets the same {"active": false} as any other
unrecognised string.
No username either¶
Toolkit's own response includes a username field when the token has a
resource-owner user attached — on LuciBook that's the user's email
address (USERNAME_FIELD = "email"), and no introspection scope grants
access to it. LuciBook strips this field from every response, including
for your own token.
You can only introspect your own app's tokens¶
LuciBookIntrospectTokenView (apps/integrations/routers/oauth.py)
resolves which application actually authenticated the request, and checks
it against the token's owning application before returning anything.
A token that exists but belongs to a different application gets
exactly the same response as a token that doesn't exist at all:
{"active": false}
— never a 403/404 that would tell you the token exists but isn't
yours. This also covers a token that's expired or was revoked: all three
cases (foreign, expired, revoked) collapse to the same {"active": false},
by design — RFC 7662 §2.2 never distinguishes them, so this endpoint
doesn't either.
Tokens carry no org claim¶
A LuciBook bearer token is scoped to (application, user), not to a
single organisation — the same token can be valid against every org that
user has connected your app to. This response never includes org_id or
org_name; if you need to know which organisation(s) a token's
connection(s) belong to, call:
GET /api/v1/connections
Authorization: Bearer <access token>
See Connections for that endpoint's shape.
Related¶
- Authorization Code + PKCE — how a token gets issued in the first place.
- Revoking tokens — the RFC 7009 counterpart, for taking a token out of service.