How to verify opaque tokens with introspection
View Sourcelivery_auth_introspect POSTs a token to the authorization
server's introspection endpoint and trusts the active field of
the JSON response (RFC 7662). You need it when your bearer tokens
are opaque reference tokens, not self-contained JWTs, so you cannot
verify them locally.
Add it to the stack
The middleware authenticates your API (the resource server) to the
authorization server with HTTP Basic.
On success the response (with scope, sub, exp, ...) is stored
under meta(user, _):
Stack = [
{livery_auth_introspect, #{
endpoint => <<"https://issuer.example/oauth/introspect">>,
client_id => <<"my-api">>,
client_secret => <<"s3cret">>
}}
%% ... handler
].Read the claims in a handler with livery_ext:user/1:
fun(Req) ->
#{<<"sub">> := Sub, <<"scope">> := Scope} = livery_ext:user(Req),
livery_resp:text(200, [<<"hello ">>, Sub, <<" (">>, Scope, <<")">>])
end.Authenticate to the authorization server another way
HTTP Basic is the default. Your authorization server may ask for
something else; client_auth takes the same methods as the token
client:
%% client_id and client_secret in the form body
#{endpoint => Endpoint, client_id => <<"my-api">>, client_secret => Secret,
client_auth => client_secret_post}
%% a signed assertion instead of a shared secret (RFC 7523)
#{endpoint => Endpoint, client_id => <<"my-api">>,
client_auth => private_key_jwt,
private_key => {<<"ES256">>, Key}, key_id => <<"k1">>,
issuer => <<"https://issuer.example">>}With private_key_jwt, the assertion's audience is the authorization
server's issuer, as RFC 10070 requires. Mutual TLS needs a client
certificate on the connection: use your own fetch for it (below).
A missing token is rejected with 401 unless required => false.
An inactive token, or any transport or decoding failure, is always
rejected with 401 and WWW-Authenticate: Bearer error="invalid_token".
Check who the token is for and what it grants
The authorization server only says whether a token is active. Whether
it is meant for your API, and whether it allows this request, is for
you to decide (RFC 7662 section 4). Set audience and scope:
{livery_auth_introspect, #{
endpoint => <<"https://issuer.example/oauth/introspect">>,
client_id => <<"my-api">>,
client_secret => <<"s3cret">>,
audience => <<"https://api.example">>,
scope => [<<"orders:read">>],
realm => <<"orders">>
}}A token for another audience gets 401. A token without one of the
scopes gets 403 with error="insufficient_scope" and the scopes it
needs, so a client knows to ask for more.
Choose JWT or introspection
| Token kind | Use |
|---|---|
| Self-contained JWT | livery_auth_bearer (local verify, no round trip) |
| Opaque / reference | livery_auth_introspect (round trip per request) |
Introspection adds a network call per request and Livery does not
cache the answers. Cache them in your own layer if the round trip is
too costly, and keep the cache shorter than the token's exp.
Plug in a custom HTTP client
By default the call goes through livery_client, verifies the
authorization server's certificate against the system trust store,
and waits 5 seconds for the answer. You can replace it: pass
fetch => fun((Url, Headers, Body) -> {ok, Status, Body} | {error, _})
to use your own client or to test without a network:
#{endpoint => Endpoint,
fetch => fun(_U, _H, _B) -> {ok, 200, <<"{\"active\":true}">>} end}Notes
client_idandclient_secretare form-encoded before they go into the Basic header, as RFC 6749 section 2.3.1 asks, so secrets with:or+work.
See also
- Reference:
livery_auth_introspect,livery_ext - Guide: Verify JWT access tokens
- Guide: Extract a bearer token