OpenFeed's FAPI 2.0 authorization flow, built on oidcc.
The flow
authorize_url/2— pushes the authorization request (PAR) and returns the front-channel URL. Keep thestate,nonceandpkce_verifierit generated; you need all three at the callback.- The consumer approves at OpenFeed and is redirected back with a
code. exchange_code/3— swaps the code for tokens.refresh/3— renews the access token when it expires.
PAR happens transparently inside authorize_url/2: OpenFeed's metadata sets
require_pushed_authorization_requests, and oidcc honours it.
Why some calls skip the FAPI2 profile
oidcc's :fapi2_security_profile is the right thing for the authorization
code flow, and this library uses it there. It cannot be used for the other
token-endpoint calls, and the reason is worth stating because it looks like an
oversight:
enforce_s256_pkce/2 sets Opts#{require_pkce => true} unconditionally,
overwriting any caller value. Grants with no authorization code — refresh and
client credentials — plus introspection therefore fail the PKCE check, and
there is no supported way to keep the profile and opt out.
So those calls drop :profiles and this module restores what that loses,
explicitly: preferred_auth_methods and trusted_audiences. The profile's
limit_signing_alg_values is unnecessary here because OpenFeed only ever
issues PS256, and its TLS hardening is inert behind a custom HTTP adapter (see
OpenFeed.ReqHttpAdapter). The authorization-relevant parts —
limit_response_types, enforce_par, enforce_iss_parameter — do not apply
to these endpoints at all.
See OpenFeed.ProviderConfiguration for the introspection metadata override
that OpenFeed's discovery document requires.
Summary
Functions
Build the consent redirect URL, pushing the authorization request first.
Get a client-credentials token, for app-level endpoints.
Exchange an authorization code for tokens.
Introspect an access token.
Generate the state, nonce and PKCE verifier for a new authorization.
Refresh an access token.
Functions
@spec authorize_url( OpenFeed.Config.t(), keyword() ) :: {:ok, String.t()} | {:error, OpenFeed.Error.t()}
Build the consent redirect URL, pushing the authorization request first.
Options
:state,:nonce,:pkce_verifier(all required) — fromnew_flow/0.:grant_management_action—"create"or"replace". Omitted by default, because OpenFeed treats it as optional and infers the action:createwhen the request carries nogrant_id,replacewhen it does. Pass"replace"with:grant_idto amend an existing consent.Note that
"query"and"revoke"appear in OpenFeed'sgrant_management_actions_supportedmetadata but are not accepted here — they are the/v1/grants/{id}endpoints. SeeOpenFeed.Sharing.grant/4andOpenFeed.Sharing.revoke_grant/4.:grant_id— the grant to amend, for"replace".:scopes— override the config's scopes for this authorization.
@spec client_credentials_token(OpenFeed.Config.t(), [atom() | String.t()]) :: {:ok, OpenFeed.Tokens.t()} | {:error, OpenFeed.Error.t()}
Get a client-credentials token, for app-level endpoints.
These are not grant-bound. Use them for /v1/app and /v1/app/grants.
Defaults to the scopes needed to list your app's grants.
@spec exchange_code(OpenFeed.Config.t(), String.t(), keyword()) :: {:ok, OpenFeed.Tokens.t()} | {:error, OpenFeed.Error.t()}
Exchange an authorization code for tokens.
Pass the nonce and pkce_verifier from the new_flow/0 call that produced
the redirect. Compare state yourself before calling this.
@spec introspect(OpenFeed.Config.t(), String.t()) :: {:ok, map()} | {:error, OpenFeed.Error.t()}
Introspect an access token.
OpenFeed's introspection response is deliberately minimal: active plus
grant_id, and nothing else.
This is how grant_id is recovered for sharing-api tokens, which are opaque
by design. exchange_code/3 and refresh/4 call it automatically when the
claim is absent; pass introspect_grant_id: false to suppress that.
Generate the state, nonce and PKCE verifier for a new authorization.
Store all three against the browser session; exchange_code/3 needs the
nonce and verifier, and you must compare state yourself on the way back.
@spec refresh(OpenFeed.Config.t(), String.t(), String.t(), keyword()) :: {:ok, OpenFeed.Tokens.t()} | {:error, OpenFeed.Error.t()}
Refresh an access token.
expected_subject must be the sub stored on the grant — oidcc verifies the
new token belongs to the same consumer.
OpenFeed does not rotate refresh tokens: rotateRefreshToken is off,
because FAPI2 permits sender-constraint (DPoP) as the alternative to rotation.
So the refresh token you hold stays valid, and concurrent refreshes are
wasteful rather than destructive. Recommended-profile refresh tokens live as
long as the grant does.
Because they live as long as the grant, a refresh that fails with
invalid_grant is reported as %OpenFeed.Error{kind: :grant_revoked} rather
than :unauthorized. Revocation is lazy — OpenFeed sweeps the refresh tokens
bound to a grant when it is revoked, so this is how a client normally learns
that a consumer withdrew consent.