An oidc application is an OpenID Connect client: your own app, or a SaaS app that supports
OIDC, signing your directory’s people in. Each directory is its own OpenID Provider, with the
issuer https://<alias>.sso.ezgh.app (endpoints). Only users you
assign to the application, directly or through a group, can sign in to it.
A directory can have up to 200 applications, of every kind. You need sso.applications.create.
For SAML 2.0 service providers, see SAML applications; to push an
application’s assigned users to it over SCIM, see Outbound provisioning.
Application types
applicationType |
For | Client authentication (tokenEndpointAuthMethod) |
|---|---|---|
web |
Apps with a server that keeps a secret | client_secret_basic (default), client_secret_post or private_key_jwt |
spa |
Single-page apps in the browser | none; PKCE (S256) required |
native |
Desktop, mobile and command-line apps | none; PKCE (S256) required |
The type can’t change after creation. Every application uses the authorization code flow;
web applications may use PKCE too, and it’s checked whenever it’s sent.
Create an application
In the console, open EZGH SSO > Applications, select Create application, and choose Web application, Single-page application or Native application.
ezgh sso applications create --name "Wiki" --type web --redirect-uri https://wiki.acme.com/callbackOr call CreateApplication:
{
"name": "Wiki",
"kind": "oidc",
"oidc": {
"applicationType": "web",
"redirectUris": ["https://wiki.acme.com/callback"]
}
}The answer is { "application", "clientSecret" }. The client ID is application.oidc.clientId,
the application’s ID. For client_secret_basic and client_secret_post, clientSecret is the
first client secret, ezgh_scs_ and 43 letters and digits. It’s shown only in this answer
(and replays of it with the same Idempotency-Key); store it now.
Then assign users or groups, and configure your app with the issuer, the client ID and the secret. Most OIDC libraries need only the issuer: they read the rest from discovery.
Settings
oidc holds the client’s settings. On update, only the fields you send change, and null
clears an optional one.
| Field | Description | Default |
|---|---|---|
redirectUris |
1 to 20 exact redirect URIs. See Redirect URIs. | Required |
postLogoutRedirectUris |
Up to 20 exact URIs that logout may return to. | None |
initiateLoginUri |
An https URL the portal tile opens. See Portal tiles. | null |
backchannelLogoutUri |
A public https URL that receives back-channel logout tokens. See Logout. | null |
jwks, jwksUri |
The client’s public keys, for private_key_jwt only (exactly one of them). See private_key_jwt. |
null |
idTokenSignedResponseAlg |
RS256 or ES256. Signs the ID token and the access token. |
RS256 |
accessTokenLifetimeSeconds |
The access token’s lifetime: 300 to 3,600. | 900 |
accessTokenAudience |
The access token’s aud: 1 to 256 characters without spaces. null is the client ID. |
null |
groupsClaim |
Which groups the groups claim lists: assigned (the user’s groups that are assigned to this application) or all (all their groups). |
assigned |
The answer also shows issuer, clientId, clientSecrets (metadata only), and for jwksUri
when the keys were last fetched (jwksFetchedAt) and the last fetch’s error (jwksFetchError).
Redirect URIs
Redirect URIs match exactly: no wildcards and no fragments.
httpsURLs.httponly on a loopback address (127.0.0.1,[::1]orlocalhost).nativeapplications may also use a private-use scheme in reverse-domain form (com.acme.app:/callback), and a loopback IP redirect URI matches any port, as RFC 8252 says.
Client secrets
web applications with client_secret_basic or client_secret_post authenticate with a client
secret. An application has at most two, so you can rotate without downtime:
- CreateApplicationSecret creates a second secret,
shown once (
409 too_many_secretswhen there are already two). - Deploy it to your app.
- DeleteApplicationSecret deletes the old one, which stops working at once.
ezgh sso applications secrets create Wiki
ezgh sso applications secrets delete Wiki <secret ID or prefix>clientSecrets lists each secret’s ID, prefix (its first 13 characters), creator, creation
time and when it was last used. Secrets are stored only as hashes: a lost secret can’t be shown
again.
private_key_jwt
A web application can instead sign a JWT with its own private key (RFC 7523) and send it as
client_assertion, with client_assertion_type
urn:ietf:params:oauth:client-assertion-type:jwt-bearer. Give its public keys as one of:
jwks: a JWK set by value,{ "keys": [...] }, of 1 to 10 RSA (2048 bits or more) or EC (P-256, P-384, P-521) signing keys, each with akidwhen there are several;jwksUri: a public https URL of the JWK set. It’s fetched hourly, and again (at most once a minute) when an assertion names akidit doesn’t have.
The assertion’s iss and sub are the client ID, its aud the issuer or the token endpoint;
it lives at most 10 minutes and can be used once. Algorithms: RS256–512, PS256–512, ES256–512.
Tokens
| Token | Format | Lifetime |
|---|---|---|
| Authorization code | Opaque | 60 seconds, single use. A second use revokes the grant. |
| ID token | JWT signed with idTokenSignedResponseAlg |
1 hour |
| Access token | JWT, RFC 9068 (typ: at+jwt) |
accessTokenLifetimeSeconds, 15 minutes by default |
| Refresh token | Opaque, ezgh_srt_…; only with the offline_access scope |
Rotated on every use. 30 days without use, and 90 days at most from sign-in. |
- A refresh token can be used once: its answer carries a new one. Presenting a used refresh token again revokes the grant, and every token in it.
- A refresh may ask for fewer scopes, never more.
- Every code exchange and refresh checks that the user is still active and still assigned; if not, the grant is revoked.
- Your APIs can validate access tokens with the directory’s JWKS, or call introspection to see revocation at once.
- Signing keys rotate every 90 days. The next key is published in the JWKS 7 days before it’s
used, and the old one stays there 7 days after. Read keys from the JWKS by
kid; don’t pin them.
Scopes and claims
| Scope | Claims (ID token and userinfo) |
|---|---|
openid |
Required for an ID token: sub (the user’s ID) |
profile |
name, preferred_username (the email address), given_name, family_name (when set), updated_at |
email |
email, email_verified (true once the user has activated) |
groups |
groups: group names, filtered by groupsClaim |
offline_access |
A refresh token |
The ID token also has iss, aud (the client ID), azp, exp, iat, auth_time,
nonce (when sent), acr, amr, sid (the portal session’s ID) and at_hash. A refreshed ID
token keeps auth_time and has no nonce.
The access token has iss, sub, aud (accessTokenAudience, or the client ID),
client_id, jti, iat, exp, auth_time, acr, amr, scope, sid, and groups with
the groups scope.
| Claim | Values |
|---|---|
amr |
pwd (password), otp (TOTP), hwk (passkey), fed (identity provider), mfa (identity provider MFA you trust) |
acr |
urn:ezgh:sso:acr:mfa for a passkey, two factors, or trusted upstream MFA; otherwise urn:ezgh:sso:acr:sfa |
Logout
- RP-initiated logout: send the person to the logout endpoint
with
id_token_hint, and optionally apost_logout_redirect_uriregistered inpostLogoutRedirectUrisandstate. - Back-channel logout: with
backchannelLogoutUri, EZGH SSO POSTs an OpenID Connect back-channel logout token (typ: logout+jwt, withsidandevents) whenever a session the application has a grant in ends: logout, sign-out on the account page, signing in again in the same browser, an administrator revoking sessions, or the user being suspended, deleted or having their factors reset. Answer200or204. Other answers (except400) are retried for up to 20 attempts.
Ending a session also revokes the application grants made in it, except those with
offline_access. Offboarding a user revokes all of them.
Portal tiles
With initiateLoginUri, assigned users see a tile for the application in the portal. The tile
opens initiateLoginUri?iss=<issuer> (OpenID Connect third-party-initiated login), and your app
starts a normal sign-in. Without it, there’s no tile.
Tile logo
Every application, of any kind, can have its own logo on its portal tile. Without one, the tile shows the application’s initials, or, for an EZGH product, the product’s own mark. An uploaded logo replaces either.
Upload it in the console, on the Logo card of the application’s Settings tab, with the CLI, or with
UpdateApplicationLogo: { "data": "<base64>" }, a PNG
or JPEG of at most 512 KiB and 1024 × 1024 pixels. It’s converted to a PNG. SVG files and links to
images elsewhere are refused. DeleteApplicationLogo
removes it. Both answer the application, and need sso.applications.update.
ezgh sso applications update wiki --logo wiki.png
ezgh sso applications update wiki --remove-logoThe application’s logoUrl is where the logo is served, on your directory’s own host:
https://<alias>.sso.ezgh.app/branding/applications/<id>/logo?v=…, or null. It’s read-only, and
a new logo gets a new URL. An application MCP Central manages can’t be given a logo
(409 managed_by_product).
Delete an application
DeleteApplication revokes every grant and token it holds, and deletes its secrets, assignments and provisioning settings.