Skip to content

Advanced OIDC and capability reference

View as Markdown

Start with Code + PKCE. Use this reference when your RP needs another response type, signed messages, or native callbacks. Read deployed discovery before configuring a library.

response_type Authorization response Required grants Front-channel ID Token hashes Refresh capability
code Code authorization_code No front-channel ID Token With refresh_token and consented offline_access
id_token ID Token with permitted profile claims implicit Neither None
id_token token ID Token and access token implicit at_hash None
code id_token Code and ID Token authorization_code, implicit c_hash With refresh_token and consented offline_access
code token Code and access token authorization_code, implicit No front-channel ID Token None
code id_token token Code, ID Token, and access token authorization_code, implicit c_hash, at_hash None

Enable every selected type explicitly. code defaults to query; Implicit and Hybrid default to fragment. All six support form_post. Implicit and Hybrid reject query. Every front-channel ID Token requires a nonempty request nonce.

Public code-bearing flows require PKCE S256. Confidential code-bearing flows should use it. Pure Implicit ignores PKCE. Pure Implicit and access-token-bearing Hybrid remove offline_access before consent. Token-endpoint ID Tokens carry at_hash for the paired access token and omit c_hash.

Parameter Use
client_id, response_type, scope Identify the client and selected flow. Scope must contain openid.
redirect_uri Exact registered callback.
state Bind the callback to your authorization transaction.
nonce Bind an ID Token to the request; mandatory for front-channel ID Tokens.
code_challenge, code_challenge_method PKCE binding for code-bearing flows. Only S256 is supported.
response_mode Select query, fragment, or form_post within the flow’s restrictions.
prompt, max_age Control interaction and proof freshness. See interaction behavior.
acr_values Space-separated authentication context preferences. Verify the returned acr before accepting a sensitive action.
id_token_hint Supply a valid ID Token for this client. It does not by itself establish a remembered login.
ui_locales Preferred interface languages; en-US and zh-CN are supported.
request, request_uri Select one signed Request Object transport.

default_max_age and default_acr_values apply when the corresponding request parameter is absent. require_auth_time requests auth_time in ID Tokens. Managed rules use the camelCase equivalents documented in ReturnRules.

Configure request_object_signing_alg="RS256" for dynamic registration, or requestObjectSigningAlg="RS256" in a managed OIDC rule. Configure the selected RP key source. The example below uses the private key generated in that guide.

Save this as signed-request.mjs. Set CLIENT_ID and REDIRECT_URI to your registered values. ISSUER defaults to the production issuer.

import { createHash, randomBytes, sign } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
const issuer = process.env.ISSUER ?? 'https://oidc.sudomimus.com';
const clientId = process.env.CLIENT_ID;
const redirectUri = process.env.REDIRECT_URI;
if (!clientId || !redirectUri) throw new Error('Set CLIENT_ID and REDIRECT_URI');
const now = Math.floor(Date.now() / 1000);
const verifier = randomBytes(32).toString('base64url');
const state = randomBytes(32).toString('base64url');
const nonce = randomBytes(32).toString('base64url');
const encode = value => Buffer.from(JSON.stringify(value)).toString('base64url');
const header = { alg: 'RS256', kid: 'rp-signing-1', typ: 'JWT' };
const payload = {
iss: clientId,
aud: issuer,
iat: now,
exp: now + 120,
client_id: clientId,
response_type: 'code',
scope: 'openid email profile',
redirect_uri: redirectUri,
state,
nonce,
code_challenge: createHash('sha256').update(verifier).digest('base64url'),
code_challenge_method: 'S256',
};
const input = `${encode(header)}.${encode(payload)}`;
const signature = sign('RSA-SHA256', Buffer.from(input),
readFileSync('rp-private.pem')).toString('base64url');
const request = `${input}.${signature}`;
writeFileSync('pending-authorization.json', JSON.stringify({
issuer, clientId, redirectUri, state, nonce, verifier,
}), { mode: 0o600, flag: 'wx' });
writeFileSync('request.jwt', request, { mode: 0o600, flag: 'wx' });
const url = new URL(`${issuer}/authorize`);
url.search = new URLSearchParams({
client_id: clientId,
response_type: 'code',
scope: 'openid email profile',
request,
}).toString();
console.log(url.toString());

Send the user’s browser to the printed URL. Keep the pending transaction on the backend. On return, validate state and iss, exchange the code with the saved verifier, and validate the ID Token nonce. The files illustrate one transaction; a deployed RP needs a store that isolates concurrent transactions and removes expired entries.

For request_uri, serve the compact contents of request.jwt from a public HTTPS URL. Send outer client_id, response_type, and scope containing openid, plus request_uri, instead of request. A nonempty registered request_uris allowance restricts references; URI fragments do not change the fetched reference.

Signed inner values override other outer parameters. Inner client_id and response_type, when supplied, must agree with the outer values. If supplied, iss must identify the client and aud must include the issuer. Time claims are validated. Unsigned, encrypted, and nested Request Objects are rejected. Do not reuse the token-endpoint client assertion as a Request Object: their audiences and contents differ.

Set userinfo_signed_response_alg="RS256" in dynamic registration, or userinfoSignedResponseAlg="RS256" in the managed rule. Then call the discovered userinfo_endpoint:

Terminal window
curl "$USERINFO_ENDPOINT" \
-H "Authorization: Bearer $ACCESS_TOKEN"

The response is application/jwt, not JSON. Verify RS256 with the provider’s discovered jwks_uri, select the exact kid, and validate iss, aud, and expiry. Match sub to the ID Token subject before using profile claims. Do not use the application’s access-token JWKS to verify signed UserInfo.

The signed response reflects current scope, policy, and consent at issuance. It is a snapshot; consent revocation cannot erase an already delivered JWT. An Accept header does not override the client’s registered response format.

Add response_mode=form_post to your normal authorization request. For example:

https://oidc.sudomimus.com/authorize
?client_id=example-rp
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foidc%2Fcallback
&response_type=code
&response_mode=form_post
&scope=openid
&state=<transaction state>
&nonce=<transaction nonce>
&code_challenge=<S256 challenge>
&code_challenge_method=S256

Your HTTP(S) callback receives a form POST such as:

POST /oidc/callback HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded
code=<authorization-code>&state=<transaction-state>&iss=https%3A%2F%2Foidc.sudomimus.com

Parse the form, then validate the saved transaction’s state and iss. Exchange the code using the original redirect URI and verifier. For Implicit or Hybrid, validate returned ID Tokens, nonce, and applicable hashes before accepting tokens.

If a cookie identifies the pending transaction, it must support cross-site POST with SameSite=None; Secure. Do not exempt the callback from transaction validation. Native custom schemes cannot receive Form Post.

Configure a managed native client with a rule such as:

{
"returnMethod": "OIDC",
"payload": {
"applicationType": "native",
"redirectUris": ["com.example.app:/oidc/callback", "http://127.0.0.1:49152/oidc/callback"],
"postLogoutRedirectUris": [],
"allowedResponseTypes": ["code"],
"allowedGrantTypes": ["authorization_code"],
"allowedScopes": ["openid", "email", "profile"],
"tokenEndpointAuthMethod": "none"
}
}

Start the system browser with Code + PKCE. Receive the response through the registered custom scheme or a loopback listener. Validate state and iss, then exchange the code with the same exact redirect URI and verifier. Do not embed a client secret or platform private key in the installed application.

Native HTTP redirects must use localhost or a loopback address. HTTPS redirects are rejected for application_type=native. Exact matching includes the port; the example does not permit an arbitrary loopback port. Dynamic registration of a custom scheme with no host also requires a sector document for approved placement. See registration metadata.

The provider supports pairwise subjects, RS256 signing, and the response types above. It does not support OAuth-only response_type=token, anonymous dynamic registration, RAT-authorized metadata updates, or encrypted ID Tokens, UserInfo, and Request Objects.

Third-party initiation uses the RP’s registered initiate_login_uri. The RP accepts GET and POST, validates iss, and starts its normal authorization transaction. Issuer-host WebFinger is available at /.well-known/webfinger; use it for issuer discovery rather than assuming an email-domain mapping.