# 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](https://www.udibo.com/docs/identity/get-started) 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=none` request from a public third-party client returns `consent_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. 1. Open your tenant's **Applications** page and choose **Create application**. Enter a **Name** for the machine application. 2. 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. 3. 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. 4. 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](https://www.udibo.com/docs/identity/apis#call-a-tenant-host-api-from-your-backend). Resource grants also require separate **Machine permissions**. Continue with [resource-grant setup](https://www.udibo.com/docs/identity/permissions#prepare-a-machine-client-for-resource-grants). ## 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](https://www.udibo.com/docs/identity/applications#register-a-machine-application), then [resource-grant setup](https://www.udibo.com/docs/identity/permissions#prepare-a-machine-client-for-resource-grants) 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](https://www.udibo.com/docs/identity/permissions), 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](https://www.udibo.com/docs/oauth2/guides/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](https://www.udibo.com/docs/identity/account-protection#set-session-lifetimes), 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](https://www.udibo.com/docs/identity/webhooks) 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](https://www.udibo.com/docs/identity/concepts), or use [troubleshooting](https://www.udibo.com/docs/identity/troubleshooting) to diagnose an integration failure. _Last verified 2026-10-30._