Configure an application
An application registration tells your tenant which client is signing someone in, where it can send the browser afterward, and which token behavior to use. Configure it in your tenant's Applications page. Start with the first-sign-in guide if you have not connected an app yet.
Keep environments separate
Register development and production as separate applications with different client credentials. For stronger separation of user data and sign-in policy, use separate tenants as well: changing the application environment does not create a separate user directory.
Use Development for local web callbacks such as
http://localhost:8000/auth/callback. Use Production with exact HTTPS web
callbacks. Development wildcard patterns are not allowed in production.
Native-app loopback IP callbacks and custom schemes have different rules from a hosted web callback. For a web project, use an exact HTTPS address in production instead of relying on a native-client exception.
Choose the client type
| Your application | Client configuration | Where credentials live |
|---|---|---|
| Web app with a backend or BFF | Confidential | Client secret and tokens on the backend |
| Browser-only app or installed native app | Public | No client secret; code shipped to users cannot keep one |
| Background service | Separate machine application | Server-side credentials and permitted scopes; a service account is created with it |
For a browser app with a backend, use the confidential BFF path even if the UI is a React SPA. The backend is the OAuth2 client in this design.
When people see the consent screen
The application's Type decides whether your tenant asks the person to approve the requested scopes. Its client configuration decides whether an earlier approval is reused:
First-party: no consent screen, unless the request sends
prompt=consent. Choose it for an app you build.Third-party, confidential: the consent screen appears the first time, and again when a request adds a scope. Later requests within the approved scopes skip it.
Third-party, public: the consent screen appears on every authorization request, including one for scopes the person approved before. A public client has no secret, so your tenant cannot tell the registered app from another program on the same device that uses its client ID. A
prompt=nonerequest from a public third-party client returnsconsent_required.
Every approval appears on the person's /connected-apps page, whichever type
of client asked for it.
A third-party application's refresh tokens work only while the person's
approval stays on record. Changing an application from first-party to
third-party therefore ends its existing sign-ins: a first-party sign-in
records no approval, so each refresh token is refused at its next refresh
(invalid_grant), and the person sees the consent screen when they sign in
again. Access tokens already issued keep working until they expire. Changing
from third-party to first-party keeps existing sign-ins.
Machine applications cannot combine Client Credentials with Authorization Code.
Registering one creates a service account for it, shown on the application's
page. This service account represents the machine and is independent of the
person who registered the application. In a machine JWT access token, sub and
client_id are the application's client ID. After validating the token, use
client_id to identify the calling application; a present sub does not prove
that a person signed in.
Revoking a secret stops new token requests with that secret; it does not invalidate already-issued access tokens. Deleting the application retires it for online token validation. A signed JWT accepted offline can remain valid until its expiry.
A service account signs in nowhere, holds no password and no email address, and is never an administrator. It is kept out of the users list unless you ask for it: the Account type filter offers "People" (the default), "People and service accounts", and "Service accounts". A user export is the exception and carries every account, service accounts included, because an export is everything the tenant holds.
Register a machine application
For a backend that places resource grants, register a separate application from the one that signs people in. These dashboard steps require JavaScript.
Open your tenant's Applications page and choose Create application. Enter a Name for the machine application.
Under Grants, check only Client credentials; leave Authorization code unchecked. A machine application is always a confidential client, so the form shows that in place of the Confidential client checkbox.
Under Allowed scopes, select Organizations — read (
identity:organizations:read) for listing resource grants and Organizations — write (identity:organizations:write) for placing or revoking them. Select both when the integration needs both operations. An empty selection permits no scopes. You must hold the corresponding dashboard permissions to allow those scopes.Choose Create application, then Create new secret on its detail page. Copy the one-time value into your backend's secret manager.
The allowlist sets which scopes a token may receive; your backend must still request the scopes it needs when obtaining a token. See the backend token-request example.
Resource grants also require separate Machine permissions. Continue with resource-grant setup.
Register the callback exactly
The redirect URI is the endpoint in your app that finishes the login, such
as https://app.example.com/auth/callback. It is different from the callback
you register at a social provider, which returns to the identity service.
Check the scheme, hostname, port, path, and query. A callback configured on
localhost is not the same address as one configured on 127.0.0.1. A change
to your reverse proxy, preview URL, or auth mount may require updating both the
registration and your backend configuration.
Use an application-controlled return path after the callback. Do not accept arbitrary external redirect destinations from a browser parameter.
A production application matches each redirect URI exactly and requires
https, with one exception for native and MCP clients: a loopback callback
such as http://127.0.0.1/callback or http://[::1]/callback is accepted on
any port, so the port the operating system assigns does not have to be
registered. localhost is not a loopback address here, because it resolves
through the device's hosts file; use the IP literal. A development application
also accepts http on any host, such as http://localhost:3000/callback, and
one wildcard label per redirect URI, such as
https://myapp-*.myorg.deno.net/callback for preview deployments. The *
stands for one label of a subdomain you control and never spans a dot.
Register where sign-out returns
A post-logout redirect URI is where sign-out may send the browser after it
ends the person's session with your tenant. Without one, your app can sign
someone out locally while their tenant session stays live, and the next sign-in
skips the form. Post-logout URIs are matched exactly, trailing slash included,
with no wildcards. http://127.0.0.1:3000/ and http://[::1]:3000/ are
accepted on any application and http://localhost:3000/ only on a development
one; unlike a redirect URI, the port you register is the port that has to come
back.
Every response the callback receives, with a code or an error, carries an iss
parameter naming the issuer that answered (RFC 9207), and discovery advertises
authorization_response_iss_parameter_supported. Before redeeming the code,
check that iss is exactly the issuer you sent the request to, and refuse a
response without one. The @udibo/oauth2 client and BFF make this check for
you when configured with your issuer. If the authorize request goes to a
different origin than the issuer it discovers, name that origin through the
BFF's resolveOrigin or login({ origin }) so the check accepts it.
Select grants and identity scopes
For browser sign-in with session renewal, check Authorization code and Refresh token. Keep PKCE and state validation enabled in the client.
Client credentials cannot be combined with Authorization code on one application: a machine credential acts as the application itself, so register the machine integration as its own application. For Client credentials, follow machine application registration, then resource-grant setup when your backend needs to list, place or revoke resource grants.
Request openid for OIDC sign-in, profile for supported profile claims, and
email when your application needs the address or verification status. Read
those claims from UserInfo when needed; do not assume the access token is a
complete user profile.
OAuth2 scopes and your application's permissions answer different
questions. A scope limits what a token can request. An application permission
describes what this user may do, such as projects:archive. Register and assign
those capabilities through
permissions and roles, and
enforce them on the backend.
Choose how your API validates tokens
| Format | Validation | Revocation and permission freshness |
|---|---|---|
| Opaque | Ask the issuer's introspection endpoint | The service checks live token state; caching your own introspection result delays that observation |
| JWT | Verify signature, issuer, audience, and expiry using JWKS | Offline verification observes the token's issued claims until expiry; use an online check where you need current state |
For JWT access tokens, configure the intended API audience. Leaving it blank
uses the client ID. Your API must reject a token intended for another audience.
Hosted introspection requires a confidential application's credentials and only
answers for tokens issued to that application. Public client IDs and another
application's credentials receive { "active": false }, including for refresh
tokens. A separate API serving public or multiple client applications should use
JWT access tokens with its explicit API audience; registering a confidential
client alone does not grant cross-application introspection access.
See Protect an API for the checked implementation of each mode.
Set token lifetimes
The application form's Token settings take each lifetime as a number and a unit (minutes, hours or days). A stored value is shown in the largest unit that holds it exactly, so 5,400 seconds reads as 90 minutes.
| Setting | Default | Range | What it bounds |
|---|---|---|---|
| Access token lifetime | 1 hour | 1 minute to 1 day | How long one access token stays valid |
| Refresh token lifetime | 14 days | 1 hour to 30 days | How long the application can go unused |
| Maximum refresh token lifetime | No maximum | 1 hour to 365 days | How long one chain of refreshes can last in total |
Access token lifetime. Clients trade their refresh token for a new access token when it expires, so a shorter value limits what a leaked token is worth without signing anyone out. For JWT access tokens it is also your revocation window.
Refresh token lifetime. Every refresh issues a new token with a fresh lifetime, so this is an inactivity window, not a cap on total signed-in time. It runs on its own clock: the session idle timeout counts any authenticated request, while this counts only token refreshes. On a first-party application it must be at least the tenant's idle timeout, or someone reading pages, which refreshes no token, is signed out while still active. Third-party applications have no session with your tenant, so that floor does not apply to them.
Maximum refresh token lifetime. The ceiling on one chain of refreshes, counted from the first token. When it runs out the client must request new authorization, which is silent for a browser app whose tenant session is still live. Leave it blank for no ceiling. It cannot be shorter than the refresh token lifetime.
Read authentication evidence
ID tokens, JWT access tokens, UserInfo and introspection can include
auth_time, acr and amr when Udibo has verified evidence for them.
auth_time is the original authentication time in Unix seconds; exchanging a
code or refreshing a token does not make it newer. Later sign-ins do not change
an existing credential's evidence.
| Authentication event | ACR | AMR |
|---|---|---|
| Password | urn:udibo:acr:pwd | pwd |
| Email code | urn:udibo:acr:pwd | otp |
| Magic link | urn:udibo:acr:pwd | Omitted |
| Password plus TOTP | urn:udibo:acr:mfa | pwd, otp, mfa |
| Password plus recovery code | urn:udibo:acr:mfa | Omitted |
| Email sign-in plus TOTP or recovery | Omitted | Omitted |
| External provider, with or without a local challenge | Omitted | Omitted |
| Legacy or machine credential | Omitted | Omitted |
The baseline pwd ACR means one verified factor; it does not by itself assert a
password. Two-factor assurance requires proofs from one bounded transaction.
Email sign-in followed by a local challenge has a verified completion time but
no asserted factor category. External-provider callbacks, legacy sessions and
machine credentials have no asserted auth_time.
Treat absent evidence as unknown. Verify the credential normally before using its claims.
Request fresh authentication
Send these parameters on the authorization request:
| Parameter | Behavior |
|---|---|
max_age=N | Require a verified local authentication event no more than N seconds old. N must be a nonnegative integer. Zero always requests a new event. |
prompt=login | Request a new local authentication event, even with an existing session. |
acr_values=urn:udibo:acr:mfa | Prefer password plus a local second factor. This is a voluntary preference: inspect the achieved acr before allowing your sensitive action. |
prompt=none | Permit no sign-in or consent UI. Returns login_required or consent_required when that interaction is needed. This prompt must be used alone. |
prompt=consent | Ask for consent again, including for a first-party application. Requires a nonempty scope. |
prompt=create | Start a signed-out user at account creation. |
The supported ACR values are urn:udibo:acr:pwd and urn:udibo:acr:mfa; MFA
also satisfies the baseline. Discovery advertises these values and the supported
prompts. Unsupported values, duplicate control parameters and malformed ages
return invalid_request after the client and redirect have been validated.
Freshness requires verified local evidence. An external-provider callback or
legacy session timestamp cannot satisfy it. When no eligible local method is
available, the request returns login_required. The hosted completion is bound
to the original request and session, expires after ten minutes and can issue
only one authorization code. A successful max_age=0 completion continues
without another sign-in, including after a consent pause.
Tenant MFA policy still applies. With optional MFA, an unenrolled password user may accept or decline the requested second factor. With policy off, new enrollment is not offered; an existing factor is always challenged. Declining or being unable to meet a voluntary preference returns only the assurance actually achieved. Your application must refuse its sensitive action when the returned evidence falls short.
For a BFF, pin max_age and acr_values in server-side extraParams. They
cannot be browser-forwarded through forwardedParams. Reauthorization does not
replace any separate proof required by Udibo's account-management APIs.
Create and rotate secrets
After saving a confidential client, open its detail page and choose Create new secret. Copy the one-time value into the backend secret manager. If the action is unavailable, your tenant access must include secret management.
A new secret expires one year after it is created unless you choose another
Expiration, including Never. The Management API applies the same
default when a request omits expiresAt; send "expiresAt": null for a secret
that never expires. Changing the default does not change secrets that already
exist.
To change an existing secret's expiry, send
PATCH /api/identity/{tenantId}/applications/{id}/secrets/{secretId} with a new
expiresAt, or with "expiresAt": null to make it never expire. A body that
leaves expiresAt out changes nothing and returns the secret as stored. Until
2026-10-30 an omitted expiresAt cleared the expiry; send null explicitly if
you relied on that.
Expiry warnings
When an application's secret is 30 days from expiring, and again at 7 days, Udibo emails everyone who can manage application secrets on your tenant, and your tenant's owners: at most five people per tenant, owners first. Each run sends each person one email per tenant that lists every secret due a warning, naming the tenant, each application and its expiry, with a link to each application's page (the first ten are listed, the rest are on the Applications page). The run is daily; when sending fails it is retried, so a person may get up to three of these emails on a bad day. Each person hears each warning once: the 30-day one and the 7-day one, per secret per expiry date. Changing a secret's expiry starts its warnings again for the new date. No warning is sent while the application has another unrevoked secret that is valid for more than 30 days, or never expires, because the application keeps signing in with that one. Revoke the expiring secret once nothing uses it.
Each warning is also recorded as an application.secret.expiring audit event,
with the application as its target and the secret's id, expiry and threshold
in its metadata. Subscribe a
webhook to
application.secret.expiring to route the warning to your own on-call tool.
For a rotation, create a replacement, deploy it to all application instances, verify a new login and refresh, and then revoke the previous secret. Coordinate the overlap with your deployment; deleting the old value before every instance has moved can interrupt authentication.
Next: learn the identity concepts, or use troubleshooting to diagnose an integration failure.
Last verified 2026-10-30.

