Skip to main content

OAuth 2.0 & OpenID Connect

LumoAuth implements OAuth 2.0 and OpenID Connect (OIDC) as its core authorization and authentication framework. OAuth 2.0 defines the authorize and token endpoints and the standard grant types (authorization code, client credentials, refresh token, etc.); OIDC adds an identity layer on top with an id_token (a signed statement of who signed in) and a userinfo endpoint. This guide covers grant types, token management, OIDC features, and protocol extensions.


Supported Grant Types​

Grant TypeUse CaseStandard
Authorization CodeWeb apps, SPAs (with PKCE)OAuth 2.0
Authorization Code + PKCEPublic clients (SPAs, mobile)PKCE (a one-time secret that protects the sign-in code)
Client CredentialsMachine-to-machineOAuth 2.0
Refresh TokenRenew expired access tokensOAuth 2.0
Device AuthorizationCLI tools, smart TVs, IoTDevice authorization (sign in on another device)
CIBADecoupled authentication (call center, POS)Client-initiated backchannel authentication

In the portal, the grant types an application may use are selected under Applications → your app → Configuration → Grant types (click Save configuration afterwards).

The Grant types section of an application's Configuration tab


Authorization Code Flow​

The most common flow for web applications:

1. App redirects user to:
/orgs/{orgId}/api/v1/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&scope=openid profile email
&state=random_state

2. User authenticates (login page)

3. LumoAuth redirects back with authorization code:
https://your-app.com/callback?code=AUTH_CODE&state=random_state
&iss=https://your-domain.com/orgs/{orgId}/api/v1
(check iss == discovery issuer and state == the value you sent)

4. App exchanges code for tokens:
POST /orgs/{orgId}/api/v1/oauth/token
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://your-app.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Redirect URIs​

The redirect_uri you send must match a registered URI exactly (scheme, host, port, path, query). What you can register:

Client typeAllowed redirect URIs
Web (default)https://... only
Native (set Configuration → Application metadata → Application type to Native application (mobile / desktop))https://..., loopback http://127.0.0.1:<port>/... / http://localhost/... / http://[::1]/..., or a private-use scheme such as com.example.app:/callback
AnyNever javascript:, data:, vbscript:, blob:, file: or about:; the portal, the admin API and dynamic registration all refuse them

The same rules apply to post-logout redirect URIs. client_uri, logo_uri, policy_uri and tos_uri must be http(s). Details: Redirect URI rules.

Redirect URIs are managed on the application's Configuration tab, under Redirect URIs. The Application type selector is further down the same tab, in the collapsed Application metadata section.

The Application metadata section of the Configuration tab with the Application type selector

Hosted login page​

When the user is not signed in, /authorize shows the organization's login page, or the application's own branded page if Applications → your app → Branding → Login page → Enable custom login page is on. Both are protected by a CSRF token, per-IP and per-email throttling, a lockout after 5 failed attempts, and a single generic error for every failure. Applications that render their own sign-in form can use the JSON endpoint POST /orgs/{orgId}/api/v1/oauth/login/json, which establishes the session and then continues through /authorize; it never returns tokens. See Hosted login page.

The Branding tab of an application with the Enable custom login page switch and live preview

Requiring MFA for an application​

Turn on Security → Authentication → Require multi-factor authentication on the application, then click Save auth settings, to make /authorize insist on a second factor for its users: users without a verified factor in the session go through step-up (or enrollment), and prompt=none returns interaction_required. The former Enable MFA and Enterprise SSO switches are gone; they had no effect. See Requiring MFA for one application.

The Authentication section of the Security tab with the Require multi-factor authentication switch

With PKCE (for Public Clients)​

Add PKCE parameters for SPAs and mobile apps:

Step 1: Generate code_verifier and code_challenge

Step 2: Include in authorization request:
&code_challenge=CHALLENGE
&code_challenge_method=S256

Step 3: Include in token request:
&code_verifier=VERIFIER

Client Credentials Flow​

For machine-to-machine authentication (no user involved):

curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/oauth/token \
-d grant_type=client_credentials \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d scope="api:read api:write"

Refresh Token Flow​

Exchange a refresh token for a new access token:

curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET

OIDC Features​

Discovery​

Every organization exposes an OIDC discovery document:

GET /orgs/{orgId}/api/v1/.well-known/openid-configuration

Returns endpoints, supported scopes, signing algorithms, and more. The document's issuer is https://your-domain.com/orgs/{orgId}/api/v1 on the organization path and the bare domain (https://auth.acme.com) on a verified custom domain. jwks_uri, registration_endpoint, etc. are advertised as absolute URLs. Always read endpoint locations and the issuer from discovery rather than hard-coding them.

Every token's iss claim, the iss field in introspection responses and the iss parameter on authorization responses equal that issuer byte-for-byte. Configure your OIDC library with the discovery issuer and leave issuer validation on.

Standard

The issuer identifier in the response and OpenID Connect Core 3.1.3.7: compare iss with the discovery issuer before trusting an authorization code or a token. This is the one check that defeats mix-up attacks. See the integrator checklist.

ID Tokens​

ID tokens are JWTs containing user identity claims:

{
"iss": "https://your-domain.com/orgs/acme-corp/api/v1",
"sub": "user-uuid",
"aud": "client-id",
"exp": 1706403600,
"iat": 1706400000,
"nonce": "random-nonce",
"email": "alice@acme.com",
"name": "Alice Smith",
"email_verified": true,
"auth_time": 1706399990,
"amr": ["pwd", "otp", "mfa"],
"acr": "urn:lumoauth:acr:mfa"
}

amr lists how the user signed in and acr the strongest MFA tier completed (omitted when no second factor was completed in the session). Access tokens carry the same two claims. Discovery lists both in claims_supported and the supported acr values in acr_values_supported; request a minimum level with acr_values — see Authentication context.

UserInfo Endpoint​

curl https://your-domain.com/orgs/{orgId}/api/v1/oauth/userinfo \
-H "Authorization: Bearer {access_token}"

Standard Scopes​

These are the OIDC scopes advertised in scopes_supported and honored by the token/userinfo endpoints:

ScopeClaims Included
openidsub
profilename, given_name, family_name, picture
emailemail, email_verified
phonephone_number, phone_number_verified
addressaddress
offline_access(no claims — requests a refresh token)
Roles and permissions in tokens

roles and permissions are not standard OIDC scopes and are not honored if requested as scopes. To include role claims in tokens, tick User roles in the application's token settings (Applications → your app → Security → Token settings → JWT claims, then Save token settings) rather than requesting a scope. The same section has a Static custom claims (JSON) field for fixed per-client claims.

The Token settings section of an application&#39;s Security tab, with token format, lifetimes and JWT claims

JWKS Endpoint​

JWKS (JSON Web Key Set) is a JSON document listing the public keys a service uses to sign its JWTs. Clients fetch it to verify access and ID tokens.

GET /orgs/{orgId}/api/v1/.well-known/jwks.json

Advanced Protocol Extensions​

DPoP (sender-constrained tokens)​

DPoP binds an access token to a key pair held by the client. Every API call must include a fresh signed proof, so a stolen bearer token alone cannot be replayed by an attacker.

# Include DPoP proof header in token request
curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/oauth/token \
-H "DPoP: eyJ..." \
-d grant_type=authorization_code \
-d code=AUTH_CODE \
-d client_id=YOUR_CLIENT_ID

The resulting token is bound to the DPoP key and can only be used with a matching DPoP proof.

PAR (pushed authorization requests)​

Pushed authorization requests let the client POST the authorization parameters to the server first and then redirect the user with a short opaque request_uri. This keeps sensitive parameters out of the browser address bar and front channel.

# Push authorization request
curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/oauth/par \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d response_type=code \
-d redirect_uri=https://your-app.com/callback \
-d scope="openid profile"

# Response: {"request_uri": "urn:ietf:params:oauth:request_uri:...", "expires_in": 60}

# Redirect user with the request_uri
/orgs/{orgId}/api/v1/oauth/authorize?client_id=YOUR_CLIENT_ID&request_uri=urn:ietf:params:oauth:request_uri:...

RAR (rich authorization requests)​

Rich authorization requests extend OAuth with a structured authorization_details parameter so the client can request fine-grained, typed authorization (for example a specific payment amount or a specific set of account operations) instead of only flat scope strings.

{
"authorization_details": [
{
"type": "payment_initiation",
"instructedAmount": {"currency": "EUR", "amount": "123.50"},
"creditorName": "Merchant Inc.",
"creditorAccount": {"iban": "DE02..."}
}
]
}

Dynamic client registration​

Dynamic client registration is a JSON API for registering OAuth clients at runtime instead of through the admin portal. Paired with the discovery document, clients can discover endpoints and then self-register.

curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/connect/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My App",
"redirect_uris": ["https://my-app.com/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_post"
}'

Client ID Metadata Documents (CIMD)​

Client ID Metadata Documents let a client skip registration altogether: it publishes a JSON document about itself at a public https:// URL and uses that URL as its client_id. LumoAuth fetches and validates the document when the client first signs a user in. This is the method the MCP authorization specification prefers, and what Claude's connectors use. It is on by default on every plan; switch it off under Security → Sign-in settings → Platform security if your organization should accept only applications registered here. The clients that connected this way are listed under Applications → External clients (CIMD), where you can also check any document before it is used. See Client ID Metadata Documents and the sample client.

CIBA (Client-Initiated Backchannel Authentication)​

Initiate authentication from a backend service:

curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/oauth/bc-authorize \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d scope="openid" \
-d login_hint="alice@acme.com" \
-d binding_message="Approve login from call center"

The user receives a push notification or out-of-band prompt to approve.


Token Formats​

TokenFormatDescription
Access TokenJWT (JWT access token profile)A signed JSON Web Token, verifiable without a server call using the public keys at the JWKS endpoint
Refresh TokenOpaqueMust be exchanged at the token endpoint
ID TokenJWTOIDC identity claims, also a signed JSON Web Token
Authorization CodeOpaqueShort-lived, single-use

Logout​

Sign users out with the discovery end_session_endpoint (/orgs/{orgId}/api/v1/oauth/logout) and an id_token_hint; register a Back-channel logout URI (Applications → your app → Configuration → Logout) so LumoAuth can end your session when the sign-out starts elsewhere. Do not link your app to the hosted /logout route: it requires a CSRF token that only LumoAuth-rendered pages have. The check_session iframe and session_state implement OpenID Connect Session Management for browser apps. See Logout.


Token Revocation​

Clients call the token revocation endpoint to invalidate an access or refresh token immediately (for example on logout or when a token is compromised).

curl -X POST https://your-domain.com/orgs/{orgId}/api/v1/oauth/revoke \
-d token=TOKEN_TO_REVOKE \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET

Standards used on this page​

Plain nameStandard
OAuth 2.0 (the core sign-in and token protocol)RFC 6749
OpenID Connect (OIDC)OpenID Connect Core 1.0
Proof Key for Code Exchange (PKCE)RFC 7636
Device authorization (sign in on another device)RFC 8628
Client-initiated backchannel authentication (CIBA)OpenID CIBA Core 1.0
Issuer identifier in the responseRFC 9207
JSON Web Key (JWK) and key sets (JWKS)RFC 7517
Sender-constrained tokens (DPoP)RFC 9449
Pushed authorization requests (PAR)RFC 9126
Rich authorization requestsRFC 9396
Dynamic client registrationRFC 7591 and RFC 7592
Discovery documentRFC 8414 and OpenID Connect Discovery
JWT access token profileRFC 9068
JSON Web Token (JWT)RFC 7519
LogoutOpenID Connect RP-Initiated, Front-Channel, Back-Channel Logout and Session Management
Token revocationRFC 7009

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