Skip to main content

Token Endpoint

The token endpoint is where your application exchanges credentials for access tokens. A grant type is the way an app proves it may have a token. The endpoint supports several: authorization code (for interactive user logins), client credentials (for service-to-service calls), refresh token (for extending sessions without re-prompting the user), device code (sign in on another device), CIBA (approval on the user's own device), token exchange (for delegation and impersonation) and the JWT bearer grant (redeeming a signed assertion from a trusted identity provider, used by Cross App Access). It is the OAuth 2.0 token endpoint; terms are explained in the glossary.

POST /orgs/{orgId}/api/v1/oauth/token

Do this​

  1. Authenticate the client: -u "CLIENT_ID:CLIENT_SECRET" (HTTP Basic) for confidential clients, client_id alone for public clients.
  2. Send the grant parameters as application/x-www-form-urlencoded.
  3. Validate the returned tokens: the iss claim must equal the discovery issuer, the signature must verify against jwks_uri, and aud must contain your client or API. See Token formats.

Authorization Code Grant​

Exchange an authorization code (received from the authorize endpoint) for access and refresh tokens. This is the most common flow for user authentication.

Request Parameters​

ParameterRequiredDescription
grant_typeYesauthorization_code
codeYesThe authorization code from the callback
redirect_uriYesMust match the URI used in the authorization request
client_idYesYour application's client ID
client_secretConfidential clientsYour application's client secret (not needed for public clients with PKCE)
code_verifierIf PKCE usedThe original code_verifier that matches the code_challenge

Example Request​

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=SplxlOBeZQQYbYS6WxSbIA" \
-d "redirect_uri=https://myapp.com/callback" \
-d "client_id=abc123def456" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

Response​

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "openid profile email",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Client Credentials Grant​

Authenticate your application as itself, without a user. Use this for server-to-server communication, cron jobs, background workers, or AI agents.

2 / 2
  1. Client App to Auth Server: POST /token (client_credentials)
  2. Auth Server to Client App: Access Token

Request Parameters​

ParameterRequiredDescription
grant_typeYesclient_credentials
client_idYesYour application's client ID
client_secretYesYour application's client secret
scopeNoSpace-separated scopes (defaults to the client's registered scopes)

Example Request​

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=read:reports write:data"

Response​

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:reports write:data"
}

Refresh Token Grant​

Get a new access token using a refresh token, without requiring the user to log in again.

A refresh token is issued only to clients whose grant types include refresh_token (authorization code, device and CIBA grants alike); otherwise the response simply has no refresh_token. Each refresh token lives for the client's refresh-token lifetime (default 30 days, 1 day to 1 year), and the whole rotation chain ends 90 days after sign-in, or after the client lifetime if that is longer. Rotation never extends that limit. See Refresh tokens.

A refresh token is refused with invalid_grant once its user is blocked, deactivated or deleted, signs out everywhere, or has their password changed or reset.

3 / 3
  1. Client App to Auth Server: POST /token (refresh_token)
  2. Note: Validate Refresh Token
  3. Auth Server to Client App: New Access Token (+ New Refresh Token)

Request Parameters​

ParameterRequiredDescription
grant_typeYesrefresh_token
refresh_tokenYesThe refresh token from a previous token response
client_idYesYour application's client ID
client_secretConfidential clientsYour application's client secret

Example Request​

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=refresh_token" \
-d "refresh_token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
Refresh Token Rotation

LumoAuth rotates refresh tokens. Each time you use a refresh token, a new one is issued and the old one is invalidated. If the old token is used again, the entire token family is revoked — this detects theft.

Token Exchange Grant​

Token exchange lets a service swap one token for another, with a narrower audience or on behalf of another party. It is the mechanism behind delegation and impersonation, and enables scenarios like:

  • Impersonation — a service acts as a user.
  • Delegation — a service acts on behalf of a user, with an audit trail.
  • Workload identity — exchange external tokens (AWS, Kubernetes) for LumoAuth tokens.

Request Parameters​

ParameterRequiredDescription
grant_typeYesurn:ietf:params:oauth:grant-type:token-exchange
subject_tokenYesThe token representing the subject (user or service)
subject_token_typeYesWhat kind of token subject_token is; see Subject and actor token types
actor_tokenFor delegationToken of the service acting on behalf of the subject
actor_token_typeIf actor_tokenWhat kind of token actor_token is; same values and rules as subject_token_type
requested_token_typeNoDefaults to an access token. Special values select the Cross App Access identity assertion or a transaction token instead
resourceNoAudience of the new token; see Resource indicators

Subject and actor token types​

Every token type names exactly one kind of credential. The server checks the token's JOSE typ header and claims against the declared type and refuses anything else, so a token can only be exchanged for what it was issued to be.

subject_token_type / actor_token_typeAcceptedResolves to
urn:ietf:params:oauth:token-type:access_tokenAn access token issued by this organization: an opaque token, or a JWT with typ: at+jwtThe user or agent the token was issued to
urn:ietf:params:oauth:token-type:jwtA JWT access token issued by this organization (typ: at+jwt), or an external workload identity token (Kubernetes, AWS, GCP, Azure, GitHub, SPIFFE) presented by a registered agent, see Workload federationThe token's user, or the agent bound to the workload identity
urn:ietf:params:oauth:token-type:id_tokenAn OpenID Connect ID token issued by this organization (typ absent or JWT, aud = the exchanging client)The user the ID token identifies
urn:ietf:params:oauth:token-type:refresh_tokenA refresh token issued by this organization to the exchanging clientThe user the refresh token belongs to

Rules that apply to every type:

  • Agents are identified by agent access tokens only. A JWT whose sub is an agent counts as the agent's credential only when it is an agent access token (typ: at+jwt with is_agent: true). A JIT permission token also carries the agent's sub, but it is task-scoped and route-bound: it is never accepted as a subject or actor token, so a downstream service that received one cannot turn it into the agent's standing identity.
  • Special-purpose JWTs are never subject or actor tokens. Back-channel logout tokens (logout+jwt), transaction tokens, identity assertions (oauth-id-jag+jwt), DPoP proofs and JARM responses are signed with the same organization key but have their own typ; the exchange refuses them whatever type is declared. A relying party that receives a logout token cannot exchange it for a new user token.
  • The subject must still be allowed to receive tokens. The exchange mints a new token, so a user who has been deactivated, blocked, deleted, or whose tokens were cut off (for example after a password reset) is refused with invalid_grant, even when the subject token itself has not expired.
  • Audience is bound to the exchanging client. For a client-initiated exchange the subject token must have been issued to (or be audienced at) the calling client; a token minted for another relying party is refused.

A type that is not listed, or a token that does not match its declared type, is answered with 400 invalid_grant (Invalid subject_token / Invalid actor_token). The response is the same in every refused case, so the endpoint cannot be used to learn why a given token was rejected.

Delegation Example​

Service B acts on behalf of User A. The resulting token has an act claim that records the chain of identity.

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=USER_A_ACCESS_TOKEN" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "actor_token=SERVICE_B_ACCESS_TOKEN" \
-d "actor_token_type=urn:ietf:params:oauth:token-type:access_token"

Resulting JWT Claims​

{
"sub": "user_a",
"act": {
"sub": "service_b"
},
"iss": "https://app.lumoauth.dev/orgs/acme-corp/api/v1",
"exp": 1704067200
}

The act (actor) claim indicates that service_b is acting on behalf of user_a. This creates a complete audit trail. The token response also carries issued_token_type (urn:ietf:params:oauth:token-type:access_token) next to the usual fields.

DPoP: sender-constrained tokens​

DPoP ties a token to a key your client holds, so a stolen copy cannot be replayed from another machine. Send a DPoP proof header (a short-lived JWT signed with that key) with any grant to receive a token bound to it (token_type: "DPoP"). Clients with DPoP-bound access tokens enabled must send one. The proof format is described on the FAPI 2.0 page.

Server-provided nonce (opt-in)​

Organizations can require a server-provided nonce in every proof (settings.security.dpop_require_nonce: true). When it is on and your proof carries no fresh nonce, the token endpoint answers:

HTTP/1.1 400 Bad Request
DPoP-Nonce: 7c1e0b9a3f2d4e5a6b7c8d9e0f1a2b3c
Content-Type: application/json

{"error": "use_dpop_nonce", "error_description": "Authorization server requires a fresh nonce in the DPoP proof"}

Implement the retry once and it works everywhere:

  1. Read the DPoP-Nonce response header.
  2. Build a new proof with that value in the nonce claim (new jti, fresh iat).
  3. Repeat the request. Nonces are single-use and scoped to your client_id.

Resource endpoints such as UserInfo do the same with 401 use_dpop_nonce. The setting is off by default; enable it only for clients that implement the retry.

Standard

Sender-constrained tokens (DPoP) bind an access token to a key the client holds, so a stolen token cannot be replayed from another machine. The standard lets the server hand out nonces to limit how long a pre-computed proof stays valid. To comply, handle use_dpop_nonce by retrying with the nonce from the DPoP-Nonce header.

Error Responses​

Token endpoint errors follow the OAuth 2.0 specification:

{
"error": "invalid_grant",
"error_description": "Authorization code is invalid, expired, or revoked"
}
ErrorHTTPDescription
invalid_request400Missing or invalid parameters
invalid_client401Client authentication failed, or the client is unknown or disabled
invalid_grant400Authorization code or refresh token is invalid or expired; PKCE verifier mismatch; DPoP key differs from the one bound to the code
invalid_scope400Requested scope is not allowed for this client
unsupported_grant_type400The grant type is not supported
unauthorized_client403The client is not allowed this grant type (for example token exchange or the device grant is not enabled for it)
invalid_target400A resource value is malformed or not registered for the client
invalid_dpop_proof400The DPoP proof is malformed, expired, replayed, or missing for a DPoP-only client
use_dpop_nonce400The organization requires a server nonce; retry with the DPoP-Nonce header value

Common mistakes​

  • Validating iss against https://app.lumoauth.dev. The issuer includes the organization path (.../orgs/acme-corp/api/v1) or is the bare custom domain. Take it from discovery.
  • Reusing a refresh token. Rotation invalidates the old one; using it again revokes the whole family.
  • Expecting a refresh token forever. The rotation chain has an absolute expiry (at least 90 days); after invalid_grant, send the user through /authorize again.
  • No refresh_token in the response. The client is not allowed the refresh_token grant.
  • Sending client_secret for a public client. Public clients authenticate with client_id plus PKCE only.
  • Retrying a DPoP proof after use_dpop_nonce without a new jti. Every proof needs a fresh jti and iat as well as the nonce.
  • Calling the token endpoint for a client that was disabled in the portal. You get invalid_client; re-enable the client or create a new one.

Revoking a token​

POST /orgs/{orgId}/api/v1/oauth/revoke

Token revocation invalidates an access or refresh token before it expires, for example when a user signs out of your app. Revoking a refresh token also revokes the access tokens issued with it. Authenticate as the client the token was issued to (HTTP Basic or client_id/client_secret in the body; public clients send client_id alone) and post the token as application/x-www-form-urlencoded:

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/revoke \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." \
-d "token_type_hint=refresh_token"
ParameterRequiredDescription
tokenYesThe access or refresh token to revoke
token_type_hintNoaccess_token or refresh_token; speeds up the lookup

The answer is always 200 with an empty JSON object ({}), whether or not the token existed, so a caller cannot probe which tokens are valid. Errors: 400 invalid_request (no token), 400 unsupported_token_type, 401 invalid_client, 429 too_many_requests (100 calls per minute). The discovery document lists the endpoint as revocation_endpoint.

Standards used on this page​

Plain nameStandard
OAuth 2.0 (the core sign-in and token protocol)RFC 6749
Device authorization (sign in on another device)RFC 8628
Client-initiated backchannel authentication (CIBA)OpenID CIBA Core 1.0
Token exchange (delegation)RFC 8693
JWT client authentication and the JWT bearer grantRFC 7523
Proof Key for Code Exchange (PKCE)RFC 7636
Resource indicatorsRFC 8707
Sender-constrained tokens (DPoP)RFC 9449
Token revocationRFC 7009

See the standards reference for what each one covers and how LumoAuth uses it.