Applications & clients
An application groups one or more clients inside a realm. A client
is the OIDC relying party your software authenticates as — it has a client_id, an authentication
method, redirect URIs, grant types, and scopes.
Create an application
From a realm, open Apps and use Create Application. Fields:
| Name (required) | Display name, e.g. Acme Web. |
| Description | Optional free text. |
| Terms & Conditions URL | Optional link. |
| Privacy Policy URL | Optional link. |
After creation you land on the application's clients page, where you add clients.
Create a client
Use the Add Client form. Fields and their exact behavior:
| Field | Notes |
|---|---|
| Label (required) | Human display name (e.g. Backend, SPA, Mobile). The client_id itself is generated by SemAuth — you don't choose it. |
| Auth Method | One of client_secret (default), none (public), or private_key_jwt. This is fixed after creation — to change it, create a new client. |
| Redirect URI (required) | Where SemAuth sends the user back after login. One URI here on creation; add more on the edit page. |
| JWKS URI | Only for private_key_jwt. The HTTPS URL where you publish your public keys. |
| Grant types | Space- or comma-separated. Defaults to authorization_code. Valid values: authorization_code, refresh_token, client_credentials. |
| Scopes | Space- or comma-separated. Defaults to openid. e.g. openid email profile offline_access. |
| Allowed API audiences | Space- or comma-separated. The API audiences this client may request (see the APIs catalog). Every value must already exist in your org's visible APIs. |
Watch the separators. Grant types, scopes, and audiences are split on spaces and
commas. Make sure each value is its own token — a stray formatting mistake can register one combined scope that
then fails at /authorize.
Public vs confidential — derived from the auth method
There is no separate "public/confidential" switch. It follows the auth method:
none→ public client. No secret; security comes from PKCE, which SemAuth requires on every authorization request.client_secret→ confidential. SemAuth generates a 32-byte secret, stores only an Argon2 hash, and shows the raw value once.private_key_jwt→ confidential. You host a JWKS URL; SemAuth never holds your private key.
The one-shot creation screen
Right after creating a client you see its Client ID (with a copy button) and, for a
client_secret client, the Client Secret labeled "save this, shown only once". This
page is served with no-cache headers and the secret can never be retrieved again — copy it now. For
none and private_key_jwt clients there is no secret to show.
Editing a client
Open a client's Edit page. You can change:
- Label
- Enabled — unchecking it disables the client; disabled clients are rejected at
/token. - Redirect URIs — one per line (multiple allowed).
- JWKS URI — shown only for
private_key_jwtclients; must behttps://. - Grant types, Scopes, Allowed API audiences
The auth method is immutable (changing it would invalidate the secret) and there is no secret-rotation control on this page — create a new client if you need a different method or a fresh secret.
Testing a client
The Test link next to each client launches the real authorization-code + PKCE flow against the
client's first registered redirect URI (with prompt=login to force a fresh login). It's the quickest
way to confirm a client is wired up correctly end-to-end before integrating your code.
Client authentication methods at /token
How each method proves the client's identity when exchanging a code or requesting tokens:
none — public client + PKCE
No client credential. The authorization code is bound to a PKCE challenge at /authorize; at
/token the client must present the matching code_verifier. Only S256 is
accepted. Use this for SPAs, mobile, and CLIs.
client_secret
The client presents its secret, verified against the stored Argon2 hash. Two transports are accepted (both map to the same method — pick whichever your library uses):
- client_secret_basic — HTTP Basic header:
Authorization: Basic base64(client_id:secret). - client_secret_post —
client_idandclient_secretin the form body.
private_key_jwt
The client signs a short-lived JWT assertion with its private key and sends it as client_assertion
(with client_assertion_type = urn:ietf:params:oauth:client-assertion-type:jwt-bearer). SemAuth fetches
your public keys from the registered JWKS URI and verifies the signature. The assertion must be RS256
or ES256; its aud must be the token endpoint URL; iss and sub
must both equal the client_id; and each jti is single-use (replays are rejected). Your
private key never leaves your infrastructure.
Redirect URI validation
At /authorize the requested redirect_uri must exactly match a
registered URI — this is the normal case for all web apps. There is one relaxation, for native/CLI clients
(RFC 8252 §7.3): if you register the bare loopback host http://localhost or
http://127.0.0.1, then any port and path on that loopback host is accepted at runtime
(http://localhost:54321/cb). This is what lets a desktop CLI pick a free port on the fly. Anything
that isn't loopback http must match exactly.
Choosing the right client for your app
| Your app | Auth method | Grant types |
|---|---|---|
| Server-side web app | client_secret or private_key_jwt | authorization_code refresh_token |
| Single-page app (SPA) | none | authorization_code refresh_token |
| Mobile / desktop / CLI | none (PKCE + loopback) | authorization_code refresh_token |
| Machine-to-machine (no user) | client_secret or private_key_jwt | client_credentials |
Note: offline_access in the requested scopes is what makes the authorization-code flow return a
refresh token. See the OIDC reference for token details.