OIDC RP Setup Guide

Register your app as a relying party against authserver.app's OpenID Connect provider

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_type must be code.
  • 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_uri client 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 the claims, acr_values, login_hint, and id_token_hint authorization 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) and max_age are honoured for every app, and ID tokens carry auth_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.1 during development.
  • Scope — space-separated, from openid (required), profile, and email. 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 exactly code; 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 %20 or +); must include openid and 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 verifier dBjftJeZ4CVP-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_grant send the user through a new authorization request.
  • An optional scope parameter 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, POST token, client_id, and client_secret to /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_uri must exactly match a registered post-logout redirect URI, and needs id_token_hint or client_id. Anything else is rejected with a 400 and no redirect.
  • With an id_token_hint for 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 sid claim. Opted-in apps are told about a logout of that session: front-channel loads your URI in a hidden iframe with iss and sid; back-channel POSTs a logout_token JWT (typ: logout+jwt). Validate it like an ID token (signature via JWKS, iss, aud, iat/exp), then check it has the http://schemas.openid.net/event/backchannel-logout event and no nonce, 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"
}
  • aud may be the issuer, the token endpoint, or the endpoint you are calling.
  • exp must be at most 5 minutes ahead. Each jti works only once.
  • Send a kid header 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_credentials means there is no end user. The client: 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 gty and client_id and no username. 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.