Skip to main content

OAuth Clients

This admin API manages OAuth 2.0 and OpenID Connect client applications within your organization. Use it to create, update, enable/disable, and delete clients, rotate client secrets, and set the scopes each client is allowed to request.

GET /orgs/{orgId}/api/v1/admin/clients
POST /orgs/{orgId}/api/v1/admin/clients
GET /orgs/{orgId}/api/v1/admin/clients/{clientId}
PUT /orgs/{orgId}/api/v1/admin/clients/{clientId}
PATCH /orgs/{orgId}/api/v1/admin/clients/{clientId}
DELETE /orgs/{orgId}/api/v1/admin/clients/{clientId}
POST /orgs/{orgId}/api/v1/admin/clients/{clientId}/rotate-secret
POST /orgs/{orgId}/api/v1/admin/clients/{clientId}/enable
POST /orgs/{orgId}/api/v1/admin/clients/{clientId}/disable
GET /orgs/{orgId}/api/v1/admin/clients/{clientId}/scopes
PUT /orgs/{orgId}/api/v1/admin/clients/{clientId}/scopes

Authentication​

All client management endpoints require an admin API key with a clients scope (admin or admin:clients:read / admin:clients:write), or a Bearer access token that carries an admin scope. The user (or the API key's creator) must also hold the endpoint's permission: clients.view to read, clients.create, clients.edit, clients.delete or clients.rotate-secret to change; see Required permissions. A plain openid profile email token gets 403.

The Client Object​

Client Object
{
"id": 38,
"clientId": "client_abc123...",
"name": "My Application",
"isConfidential": true,
"isPublic": false,
"isActive": true,
"requiresPkce": true,
"createdAt": "2026-01-15T10:30:00Z",
"redirectUris": ["https://app.example.com/callback"],
"scopes": ["openid", "profile", "email"],
"grantTypes": ["authorization_code", "refresh_token"]
}

id is a numeric identifier (not the clientId). The redirectUris, scopes, and grantTypes arrays are only included on single-client responses (GET /clients/{clientId} and on create); the list endpoint omits them. The client secret is never part of any read response.

List Clients​

List All Clients
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients?page=1&limit=50" \
-H "Authorization: ApiKey lmk_abc123"

Query Parameters​

ParameterDescription
pagePage number (default: 1)
limitResults per page (default: 25, max: 100)
searchFilter by name or client ID
isPublicFilter by client type — true for public clients, false for confidential

Create Client​

Create a Client
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"name": "My Application",
"redirectUris": ["https://app.example.com/callback"],
"scopes": ["openid", "profile", "email"],
"grantTypes": ["authorization_code", "refresh_token"],
"isConfidential": true,
"requiresPkce": true
}'
201 Created
{
"data": {
"id": 39,
"clientId": "client_abc123...",
"secret": "raw_secret_only_shown_once",
"name": "My Application",
"_note": "Store the secret securely. It will not be shown again."
},
"message": "OAuth client created successfully"
}
warning

secret is returned only once at creation time. It is stored as a bcrypt hash; neither the API nor the portal can display it again. If you lose it, rotate it.

Create Parameters​

FieldTypeDescription
namestringRequired. Display name
redirectUrisarrayAllowed redirect URIs; see Redirect URI rules
scopesarrayAllowed scopes (default: openid profile email)
grantTypesarrayAllowed grant types (default: authorization_code refresh_token)
isConfidentialbooleantrue for server-side apps, false for SPAs (default: true)
requiresPkcebooleanRequire PKCE for authorization code flow (default: true)

Redirect URI rules​

The same policy applies in the portal, this API and dynamic registration:

URIAccepted
https://app.example.com/callbackYes
http://127.0.0.1:8400/callback, http://localhost/cb, http://[::1]/cbYes at registration; used at runtime only when the client's application type is Native (set in the portal under Configuration → Application metadata → Application type). Web clients, the default, must use https.
com.example.app:/callback (private-use scheme)Yes, for native apps
http://app.example.com/callbackNo (http on a non-loopback host)
javascript:…, data:…, vbscript:…, blob:…, file:…, about:…No, ever. The request fails with 400 and the client is not created.

At authorization time the redirect_uri must match a registered entry exactly (scheme, host, port, path, query). No wildcards.

Standard

The core OAuth 2.0 standard makes the registered redirect URI the anchor of the whole authorization code flow, and OAuth for native apps describes the loopback and private-use-scheme redirects that native apps use. To comply, register exact URIs and mark installed apps as native.

Refresh tokens​

A client receives a refresh_token only when its grantTypes include refresh_token (a legacy client with an empty list keeps receiving them) and its refresh-token lifetime is above zero. offline_access is not required and does not change this.

SettingWhereRule
Refresh token lifetimePortal: Applications → your app → Security → LifetimesSliding lifetime of each refresh token; default 30 days, accepted range 1 day to 1 year.
Family expiryFixedThe chain of rotated refresh tokens from one sign-in ends after 90 days, or after the client's refresh-token lifetime if that is longer. Rotation never extends it; the user signs in again after that.

Removing refresh_token from grantTypes stops new refresh tokens, including the replacement on the next refresh, so existing sessions end when their access token expires.

Claims and roles​

Access tokens carry personal data only for the scopes that were granted, whatever the client's JWT-claims settings say: email/email_verified need email, name and the other profile claims need profile, phone_number needs phone, address needs address. The JWT claims settings (portal: Applications → your app → Security → Token settings → JWT claims, JWT access tokens) can only remove claims from that set.

roles is opt-in per client: the access token and the UserInfo response include the user's role names only when User roles is ticked and saved in the client's JWT claims. A client that never saved that setting gets no roles.

Update Client​

Use PUT to replace all fields; PATCH for partial updates. Redirect URIs are validated with the same rules as on create.

Update a Client
curl -X PATCH https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/01JF3KABCDE... \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"name": "My Application v2",
"redirectUris": [
"https://app.example.com/callback",
"https://app.example.com/auth/callback"
]
}'

Enable / Disable Client​

A disabled client is treated as unknown on every OAuth endpoint: authorize (inline error), token, PAR, CIBA, device authorization, introspection and revocation all answer invalid_client, and its hosted login page stops accepting credentials. Disabling does not revoke tokens that were already issued; revoke them first if you need them gone.

Disable a Client
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/01JF3KABCDE.../disable \
-H "Authorization: ApiKey lmk_abc123"
Response
{
"message": "Client disabled",
"data": { "id": 38, "clientId": "client_abc123...", "isActive": false }
}
Enable a Client
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/01JF3KABCDE.../enable \
-H "Authorization: ApiKey lmk_abc123"

Rotate Client Secret​

Rotation is the only way to obtain a usable secret after creation. The old secret stops working immediately; deploy the new one first where you can (the portal offers deploy targets for this). In the portal, rotation is the Regenerate link next to Client secret on the application's Credentials tab; the new secret is shown once.

Rotate Secret
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/01JF3KABCDE.../rotate-secret \
-H "Authorization: ApiKey lmk_abc123"
Response
{
"data": {
"clientId": "client_abc123...",
"secret": "new_secret_only_shown_once",
"_note": "Store the new secret securely. It will not be shown again."
},
"message": "Client secret rotated successfully"
}

The Credentials tab of an application, where the client secret is stored hashed and can be regenerated

The first-party lumoauth-cli:{orgId} client that the lumo login device flow provisions is a public client; its secret column holds a hash of a discarded random value and there is nothing to reveal or rotate.

Manage Client Scopes​

Get Client Scopes​

Get Client Scopes
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/01JF3KABCDE.../scopes" \
-H "Authorization: ApiKey lmk_abc123"
Response
{
"data": ["openid", "profile", "email", "read:reports"]
}

Set Client Scopes​

Replaces all allowed scopes for the client.

Set Client Scopes
curl -X PUT https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/01JF3KABCDE.../scopes \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"scopes": ["openid", "profile", "email", "read:reports", "write:reports"]}'

Delete Client​

Delete a Client
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/01JF3KABCDE... \
-H "Authorization: ApiKey lmk_abc123"

Returns 200 on success. Deleting a client does not revoke existing access tokens — revoke tokens first if needed.

Common mistakes​

  • 400 on create with "Invalid redirect URI". One entry uses a forbidden scheme, plain http on a non-loopback host, or has no host. Nothing is created; fix the entry and resend.
  • Loopback redirect registered but /authorize says the redirect URI does not match. The client's application type is still Web. Change Application type to Native application (mobile / desktop) on the application's Configuration tab (Application metadata section) in the portal.
  • 403 with a user's access token. The token has no admin scope; request admin (or admin:clients:write) when obtaining it, or use an API key.
  • Looking for the secret on the client detail page. It is not there. The portal's Credentials tab shows "Stored hashed and cannot be displayed" with a Regenerate action; use it or rotate-secret.
  • No refresh_token in the token response. The client's grantTypes do not include refresh_token. Add it; offline_access alone does nothing.
  • roles or email disappeared from access tokens. Since 2026-09-28 email needs the email scope, name the profile scope, and roles must be switched on explicitly (User roles) under Security → Token settings → JWT claims.
  • Disabled a client and tokens still work. Expected: disabling blocks new grants and introspection by that client, but does not revoke tokens. Use the revocation endpoint.

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
OAuth for native appsRFC 8252
Dynamic client registrationRFC 7591 and RFC 7592
Client-initiated backchannel authentication (CIBA)OpenID CIBA Core 1.0
Device authorization (sign in on another device)RFC 8628
Token introspectionRFC 7662
Token revocationRFC 7009

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