This walks a tenant administrator through registering a relying party (RP) so its users can sign in
through authserver.app's OpenID Connect provider, and completes the baseline Authorization Code + PKCE
flow. Configuring an OIDC App requires signing in to your tenant and holding the
access_management grant.
Not yet supported
Only the baseline below is implemented. The following are not available today — don't build against them:
- Implicit or hybrid response types —
response_typemust becode. - The resource-owner-password grant. The refresh_token grant is available only for apps whose admin has opted in (see step 4), and the client_credentials grant only for service clients (see step 8).
- Dynamic client registration — apps are registered by a tenant admin through this UI only.
- PAR, JAR, DPoP, mTLS, client_secret_basic, client_secret_jwt,
jwks_uriclient keys, device authorization, token exchange, and CIBA. private_key_jwt is available for apps whose admin has chosen it (see step 8). - Session management (
check_session_iframe), and theclaims,acr_values,login_hint, andid_token_hintauthorization parameters. Front-channel and back-channel logout are available only for apps whose admin has opted in (see step 7). - A consent screen is shown only for apps whose admin has turned on user consent.
prompt(none,login,consent) andmax_ageare honoured for every app, and ID tokens carryauth_time. - Per-app user/group authorization — any authenticated user of the tenant can complete the flow.
1. Register your OIDC App
Create an OIDC App record with:
- Name, description, company — identify the app and which tenant company owns it.
-
Redirect URIs — one exact callback URL per line. HTTPS is required except for
localhost/127.0.0.1during development. -
Scope — space-separated, from
openid(required),profile, andemail. Nothing else is supported. - Signing certificate — optional; leave unset to use the tenant's file-based signing key.
- Token lifetimes — authorization code, access token, and ID token lifetimes, in seconds.
- Refresh tokens — off by default. When Issue refresh tokens is on, the refresh-token lifetime (300 to 2,592,000 seconds) caps each token family.
- Logout — optional exact post-logout redirect URIs (same rules as redirect URIs), and separate opt-ins for front-channel and back-channel logout with their URIs. Back-channel URIs must be HTTPS on a publicly reachable host.
Saving generates a client_id and a client_secret. The secret is shown
once, immediately after creation (or after an explicit rotation) — copy it then, it
is never shown again on the edit or detail pages. If you lose it, rotate it for a new one.
2. Know your issuer and endpoints
Each OIDC App is its own issuer, at /oidc/<record number>/ on your tenant domain.
For app 42 on acme.authserver.app:
issuer: https://acme.authserver.app/oidc/42 discovery: https://acme.authserver.app/oidc/42/.well-known/openid-configuration authorization_endpoint: https://acme.authserver.app/oidc/42/authorize token_endpoint: https://acme.authserver.app/oidc/42/token userinfo_endpoint: https://acme.authserver.app/oidc/42/userinfo jwks_uri: https://acme.authserver.app/oidc/42/.well-known/jwks introspection_endpoint: https://acme.authserver.app/oidc/42/introspect
Point your RP's OIDC library at the discovery URL — it fetches every other endpoint and the signing algorithm from there. The OIDC App's detail page in the admin shows this discovery URL directly.
3. Send an authorization request with PKCE
S256 PKCE is required for every client — there is no way to disable it. Generate a random
code_verifier, derive code_challenge = BASE64URL(SHA256(code_verifier)), and
send the challenge with the authorization request; send the verifier later at the token endpoint.
GET /oidc/42/authorize? response_type=code& client_id=your-client-id& redirect_uri=https%3A%2F%2Fyourapp.example.com%2Fcallback& scope=openid%20profile%20email& state=a-random-csrf-value& code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM& code_challenge_method=S256
-
response_type— must be exactlycode; nothing else is accepted. -
client_id— from step 1. -
redirect_uri— URL-encoded, and must exactly match one registered URI (see step 1) — no trailing-slash or query-string leniency. -
scope— space-delimited (URL-encoded as%20or+); must includeopenidand be a subset of the app's configured scope. -
state— recommended, returned to you unchanged; use it as your CSRF token. -
code_challenge/code_challenge_method=S256— required. The value above is the RFC 7636 reference example, derived from verifierdBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.
An unauthenticated visitor is sent to the shared tenant login (and MFA, if required) and returned here
afterward — the authorization request survives that detour. On success you get back
?code=...&state=... at your exact redirect_uri.
4. Exchange the code at the token endpoint
POST to the token endpoint with client_secret_post authentication (the
client secret goes in the POST body — this is the only supported client authentication method;
client_secret_basic is not implemented):
POST /oidc/42/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code=...& redirect_uri=https://yourapp.example.com/callback& client_id=your-client-id& client_secret=your-client-secret& code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
A successful response:
{
"access_token": "<RS256 JWT>",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "<RS256 JWT>",
"scope": "openid profile email"
}
Both JWTs are signed RS256 and carry an explicit kid matching a key published at the
JWKS URI. Every field here — redirect_uri, the code, and the PKCE verifier — is
re-validated at this step; a mismatch on any of them fails the exchange.
Refresh tokens (only for opted-in apps)
If the app issues refresh tokens, the response above also contains refresh_token, and
discovery lists refresh_token in grant_types_supported. To refresh:
POST /oidc/42/token Content-Type: application/x-www-form-urlencoded grant_type=refresh_token& refresh_token=...& client_id=your-client-id& client_secret=your-client-secret
- Every refresh returns a new
refresh_token. Store it and discard the old one. -
Presenting an already-used refresh token is treated as theft. The whole token family is revoked,
including the newest token. Make only one refresh call at a time per token, and on
invalid_grantsend the user through a new authorization request. - An optional
scopeparameter may narrow, never widen, the originally granted scope. - The family has a fixed lifetime from the first sign-in, and refreshing does not extend it. It also ends when the user logs out of that browser session, is deactivated, or loses access to the app.
-
To revoke a refresh token,
POSTtoken,client_id, andclient_secretto/oidc/42/revoke(RFC 7009). Access and ID tokens are stateless JWTs and stay valid until they expire.
5. Scopes and the claims they unlock
sub (a stable opaque identifier) is always present. Every other identity claim is gated
by scope, in both the ID token and the UserInfo response:
| Scope | Claims unlocked |
|---|---|
openid (required) |
sub |
email |
email, email_verified |
profile |
name, preferred_username |
Request userinfo_endpoint with Authorization: Bearer <access_token> to
fetch these same claims independently of the ID token, filtered the same way by the token's granted
scope.
6. Read discovery and JWKS
GET the discovery URL from step 2 for a standard OpenID Provider Metadata document:
{
"issuer": "https://acme.authserver.app/oidc/42",
"authorization_endpoint": "https://acme.authserver.app/oidc/42/authorize",
"token_endpoint": "https://acme.authserver.app/oidc/42/token",
"userinfo_endpoint": "https://acme.authserver.app/oidc/42/userinfo",
"jwks_uri": "https://acme.authserver.app/oidc/42/.well-known/jwks",
"introspection_endpoint": "https://acme.authserver.app/oidc/42/introspect",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"introspection_endpoint_auth_methods_supported": ["client_secret_post"],
"code_challenge_methods_supported": ["S256"]
}
The JWKS URI publishes the current public signing key(s) only — never private material — keyed by
kid, so your RP library can verify id_token/access_token
signatures without a separate manual key exchange.
7. Log users out
Send the browser to the end_session_endpoint from discovery (RP-Initiated Logout 1.0):
GET /oidc/42/end-session? id_token_hint=the-id-token-you-received& post_logout_redirect_uri=https://rp.example.com/logged-out& state=opaque-value
-
post_logout_redirect_urimust exactly match a registered post-logout redirect URI, and needsid_token_hintorclient_id. Anything else is rejected with a 400 and no redirect. -
With an
id_token_hintfor the signed-in user's current session (an expired one is fine), the user is logged out immediately. Otherwise the user is asked to confirm. - Logout ends the browser session and revokes the refresh tokens issued to it. Access and ID tokens already issued stay valid until they expire.
-
ID tokens carry a
sidclaim. Opted-in apps are told about a logout of that session: front-channel loads your URI in a hidden iframe withissandsid; back-channel POSTs alogout_tokenJWT (typ: logout+jwt). Validate it like an ID token (signature via JWKS,iss,aud,iat/exp), then check it has thehttp://schemas.openid.net/event/backchannel-logoutevent and nononce, and respond 200. Delivery is best effort, with no retries.
8. Service clients and private_key_jwt
Discovery lists exactly one client authentication method and the grant types for your app. Both are chosen by the tenant admin.
private_key_jwt
Instead of client_secret, send a JWT signed with your private key. The admin registers
the matching public RSA (RS256, 2048 bits or more) or EC P-256 (ES256) key. Once this method is on,
the client secret is no longer accepted at the token, introspection, or revocation endpoint.
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...
assertion claims:
{
"iss": "your-client-id",
"sub": "your-client-id",
"aud": "https://acme.authserver.app/oidc/42/token",
"exp": 1760000060,
"jti": "a-new-random-id-every-time"
}
audmay be the issuer, the token endpoint, or the endpoint you are calling.expmust be at most 5 minutes ahead. Eachjtiworks only once.- Send a
kidheader when more than one key is registered.
client_credentials (service clients)
A service client has no redirect URIs and no users. It gets an access token for itself:
POST /oidc/42/token
grant_type=client_credentials&client_id=...&client_secret=...
access token claims:
{
"iss": "https://acme.authserver.app/oidc/42",
"sub": "client:your-client-id",
"aud": "your-client-id",
"client_id": "your-client-id",
"gty": "client_credentials",
"exp": 1760003600, "iat": 1760000000, "jti": "..."
}
-
Resource servers:
gty=client_credentialsmeans there is no end user. Theclient:subject never equals a user's subject, which is always digits. - No scopes are defined for service clients yet, so don't send
scope. The response has no ID token and no refresh token. - Introspection returns
gtyandclient_idand nousername. UserInfo rejects these tokens.
Validate your setup
There's no dedicated OIDC diagnostic tool yet (unlike the SAML Metadata Parser and Response Debugger
in the Tools menu). In the meantime: open your discovery URL directly in a browser to confirm it
resolves and lists exactly the endpoints/scopes above, and check that your RP's ID token validation
actually fetches and matches a kid from the JWKS URI rather than skipping signature
verification.