Skip to content

OIDC flow

View as Markdown

If you have an existing application that already speaks OpenID Connect, or you want to use an off-the-shelf OIDC library, you can integrate with Sudomimus as a standard OIDC relying party (RP). The Sudomimus OIDC provider lives at oidc.sudomimus.com and supports the authorization code flow with PKCE, the canonical modern OIDC integration shape.

Use this guide when:

  • Your framework or platform has a first-class OIDC integration (Next-Auth, Spring Security, Keycloak adapter, etc.) and you want to slot Sudomimus in as the IdP.
  • You’re integrating with a partner system that already expects an OIDC provider.
  • You prefer the OIDC mental model (clients, scopes, ID tokens) to the Connect protocol.

If you’re starting fresh and just want the smallest custom integration, the Connect protocol is usually shorter.

The official SDKs are most useful for Connect, Session, Device, Native, and token verification helpers. See the SDK overview if your OIDC application also verifies Sudomimus application access tokens or manages ApplicationSessions directly.

Sudomimus publishes a standard OIDC discovery document. Point your library at the issuer URL https://oidc.sudomimus.com and it will fetch the rest from there:

Terminal window
curl https://oidc.sudomimus.com/.well-known/openid-configuration

Sudomimus does not publish a separate OIDC OpenAPI schema. Use discovery for the deployed endpoint URLs and advertised capabilities, the OpenID Connect and OAuth standards for protocol semantics, and this guide for the supported Sudomimus profile and its platform-specific constraints. The generated OpenAPI reference covers the Connect, Session, Native, and Device product APIs instead.

The document advertises:

  • response_types_supported: ["code"] (authorization code flow only).
  • authorization_response_iss_parameter_supported: true — authorization callbacks identify the provider according to RFC 9207.
  • grant_types_supported: ["authorization_code", "refresh_token"].
  • scopes_supported: ["openid", "email", "profile", "offline_access"].
  • claims_supported includes sub, email, email_verified, name, given_name, family_name, picture, and picture_animated.
  • claim_state_endpoint identifies Sudomimus’s provider-specific endpoint for live claim policy and consent metadata.
  • id_token_signing_alg_values_supported: ["RS256"].
  • code_challenge_methods_supported: ["S256"]PKCE is required, plain not supported.
  • token_endpoint_auth_methods_supported: ["private_key_jwt", "client_secret_basic", "client_secret_post", "none"].
  • ui_locales_supported: ["en-US", "zh-CN"].

In with.sudomimus.com, on the application you want to expose via OIDC:

  1. Add a Layer 3 OIDC return rule:

    {
    "returnMethod": "OIDC",
    "payload": {
    "redirectUris": ["https://app.example.com/oidc/callback"],
    "postLogoutRedirectUris": ["https://app.example.com/"],
    "allowedScopes": ["openid", "email", "profile", "offline_access"],
    "tokenEndpointAuthMethod": "private_key_jwt"
    }
    }
  2. Add the Layer 1 and Layer 2 rules you would for any other application — at least one authentication method (e.g. PASSKEY_USERNAMELESS or PASSKEY_REASONED) and at least one realize rule (e.g. EMAIL with the addresses or domain pattern you accept). The OIDC flow runs through the same authentication challenge as the rest of the platform.

  3. Choose a client authentication method:

    • private_key_jwt (recommended for confidential clients) — your RP holds a private key and signs a JWT assertion at /token. The signing key is your application’s client-auth key, the same key used by confidential Connect integrations to sign /establish.
    • client_secret_basic (confidential clients) — your RP presents its shared secret in the HTTP Authorization: Basic header at /token.
    • client_secret_post (confidential clients) — your RP sends its shared secret in the /token form body (client_id + client_secret parameters).
    • none — only for public clients (SPAs, mobile apps without a backend). PKCE is required.

client_secret_basic and client_secret_post use the same application secret. Generate or rotate it from the application’s page in the With portal, then store it securely.

The application’s applicationAnchor is your client_id.

Redirect the user’s browser to /authorize with the standard OIDC parameters:

https://oidc.sudomimus.com/authorize
?client_id=my-app
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foidc%2Fcallback
&response_type=code
&scope=openid%20email%20profile
&ui_locales=zh-CN%20en-US
&state=<csrf-token>
&nonce=<random-nonce>
&code_challenge=<S256-of-verifier>
&code_challenge_method=S256

Required: client_id, redirect_uri, response_type=code, scope (must include openid), code_challenge, code_challenge_method=S256. Optional but recommended: state, nonce.

Use the optional ui_locales parameter when your application knows the user’s preferred interface languages. List BCP 47 tags in preference order, separated by spaces. Sudomimus uses the first supported value (en-US or zh-CN); unsupported values are ignored and never block sign-in. The hint applies only to this sign-in, and the user can still switch languages from the page.

Let your OIDC library generate PKCE values whenever possible. Create a fresh pair for every authorization attempt and keep the verifier with the pending login state until the callback arrives.

If you generate the values yourself:

  • code_verifier must be 43–128 characters using only letters, digits, -, ., _, and ~.
  • code_challenge must be the unpadded base64url encoding of the verifier’s SHA-256 digest. An S256 challenge is exactly 43 characters and uses only letters, digits, -, and _.
  • Send only code_challenge to /authorize. Send the original, unchanged code_verifier when exchanging the code at /token.

Do not reuse a verifier or copy a fixed example into production. Sudomimus rejects malformed values as well as verifier/challenge mismatches.

Most applications can omit prompt and max_age and use the normal interactive sign-in. Add them only when your application needs a specific behavior:

  • prompt=login requests fresh authentication. Sudomimus already performs fresh authentication for every interactive OIDC request.
  • prompt=consent shows the consent step again, even when the user has already made standing claim-sharing choices.
  • prompt=none is a non-interactive check. Sudomimus does not keep a reusable provider browser session, so this request returns login_required. Catch that error and retry with an interactive authorization request.
  • max_age is a non-negative number of seconds. Because interactive requests always authenticate afresh, any valid max_age is satisfied.

Request offline_access only when your application needs to refresh tokens after the user leaves or closes it. Sudomimus shows a separate, default-unchecked offline-session choice. If the user declines, sign-in still succeeds, but the returned scope omits offline_access and no refresh token is issued. Always use the returned scope and the presence of refresh_token as the result of that choice.

Sudomimus redirects the user to via.sudomimus.com where they authenticate via the methods allowed by your Layer 1 rules. After a successful authentication and realize, the browser returns to your redirect_uri with a code, the original state, and iss=https://oidc.sudomimus.com.

Authorization errors that occur after Sudomimus validates your registered callback return to that callback with error, error_description, the original state, and the same iss. Your OIDC library must compare iss exactly with the issuer saved for this login and reject a missing or different value before exchanging the code. Errors before callback validation are returned directly as JSON. Treat server_error as a temporary provider failure and let the user retry; do not cache protocol error responses.

POST the authorization code to /token. The body is application/x-www-form-urlencoded, per OIDC:

Terminal window
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://app.example.com/oidc/callback" \
--data-urlencode "code_verifier=$PKCE_VERIFIER" \
--data-urlencode "client_id=my-app" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$CLIENT_ASSERTION_JWT"

The client_assertion is a JWT you sign with your application’s client-auth private key. Required claims: iss = client_id, sub = client_id, aud = the exact token endpoint URL (https://oidc.sudomimus.com/token in production), fresh jti, iat, exp (within 300s of iat). RS256.

Successful response (JSON):

{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 10800,
"id_token": "<JWT>",
"scope": "openid email profile"
}
  • id_token — signed by Sudomimus’s platform-wide OIDC key; verify against https://oidc.sudomimus.com/.well-known/jwks.json. It carries minimal protocol claims (iss, sub, aud, exp, iat, at_hash, optional nonce and auth_time) plus authentication context (amr, acr). Profile claims come from /userinfo.
  • access_token — signed by the application’s token-signing key, with the same minimal registered and session claims as a Sudomimus application access token. It carries no profile fields.
  • refresh_token — included only if you requested offline_access and the user approved the offline session.
Terminal window
curl https://oidc.sudomimus.com/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"

Returns the claims permitted by the granted scopes:

{
"sub": "<sector subject>",
"email": "<only if 'email' scope was granted>",
"email_verified": true,
"name": "<only if 'profile' scope was granted>",
"given_name": "<only if 'profile' scope was granted>",
"family_name": "<only if 'profile' scope was granted>",
"picture": "<only if 'profile' scope was granted>",
"picture_animated": "<only if 'profile' scope was granted>"
}

sub is the sector subject — the per-sector, application-visible identifier and the same value as the id_token sub. Use it as the user’s key. /userinfo accepts both GET and POST.

The picture and private picture_animated values follow the sector-scoped avatar delivery contract; see Avatar claims and delivery.

When your application needs current policy and consent metadata, call the discovered claim_state_endpoint with the same Bearer access token. Both GET and POST are supported. The response never contains profile values:

{
"sub": "<sector subject>",
"claims": {
"email": { "requirement": "OPTIONAL", "state": "GRANTED" },
"given_name": { "requirement": "OPTIONAL", "state": "GRANTED" },
"family_name": { "requirement": "OFF", "state": "UNKNOWN" },
"picture": { "requirement": "OFF", "state": "UNKNOWN" },
"picture_animated": { "requirement": "OFF", "state": "UNKNOWN" }
}
}

Only email-scope and profile-scope entries are returned. An openid-only session receives an empty claims object.

Every id_token includes:

ClaimMeaning
amrStandard Authentication Methods References values, such as ["hwk", "user"] for passkey or ["otp"] for email OTP.
acrSudomimus-specific authentication context string, such as urn:sudomimus:acr:passkey or urn:sudomimus:acr:email-otp.

If your RP needs phishing-resistant sign-in for a sensitive action, check acr for urn:sudomimus:acr:passkey. Federated upstreams such as Google, GitHub, Discord, Battle.net, X, Steam, and enterprise federation map to amr: ["pwd"]; use acr when you need to distinguish the upstream provider.

If you requested offline_access, the user approved it, and you received a refresh token, exchange it at /token:

Terminal window
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
--data-urlencode "client_id=my-app"
# plus client_assertion for confidential clients

Use the same client authentication method as the authorization-code exchange. For client_secret_basic, use -u "my-app:$CLIENT_SECRET" and omit the body client_id. For client_secret_post, include both body client_id and client_secret.

You can optionally pass scope to request a narrowed subset of the originally-granted scopes. Per OIDC §12.1, the ID token from a refresh does not include a new nonce.

https://oidc.sudomimus.com/end-session
?id_token_hint=<id_token>
&client_id=my-app
&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2F
&state=<optional>

/end-session signals the end of an OIDC session and redirects to the post_logout_redirect_uri (which must match one of the registered URIs exactly). When id_token_hint verifies and its aud matches this client, Sudomimus advances account/application session authority and revokes older ApplicationSessions for that account within this application.

A bad, forged, or stale hint does not block logout redirection; it simply means no application sessions are revoked. The hint may be expired, which lets an RP still drive logout after its local session has timed out.

Use Session API /logout when you hold a specific refresh token and want to revoke only that one session. Use Session API /revoke-all from a backend when you need application-wide account revocation without relying on an id_token_hint. See Managing sessions.

Most modern OIDC libraries (Node’s openid-client, Python’s authlib, Java’s nimbus-jose-jwt, etc.) discover everything from the issuer URL and handle PKCE, JWKS, and token verification automatically. This Node example supports openid-client 6.x (tested with 6.8.4) and uses a public, PKCE-only client registered with token_endpoint_auth_method: "none":

import * as oidc from "openid-client";
const redirectUri = "https://app.example.com/oidc/callback";
const configuration = await oidc.discovery(
new URL("https://oidc.sudomimus.com"),
"my-app",
{
redirect_uris: [redirectUri],
response_types: ["code"],
token_endpoint_auth_method: "none",
},
oidc.None(),
{
execute: [oidc.enableNonRepudiationChecks],
},
);
export const startSignIn = async (savePendingSignIn) => {
const codeVerifier = oidc.randomPKCECodeVerifier();
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
const state = oidc.randomState();
const nonce = oidc.randomNonce();
await savePendingSignIn({
codeVerifier,
state,
nonce,
});
return oidc.buildAuthorizationUrl(configuration, {
redirect_uri: redirectUri,
scope: "openid email profile",
code_challenge: codeChallenge,
code_challenge_method: "S256",
state,
nonce,
});
};
export const finishSignIn = async (
callbackUrl,
consumePendingSignIn,
createServerSideApplicationSession,
) => {
const pendingSignIn = await consumePendingSignIn();
if (pendingSignIn === undefined) {
throw new Error("OIDC sign-in state is missing or expired");
}
const tokens = await oidc.authorizationCodeGrant(
configuration,
callbackUrl,
{
pkceCodeVerifier: pendingSignIn.codeVerifier,
expectedState: pendingSignIn.state,
expectedNonce: pendingSignIn.nonce,
},
);
const claims = tokens.claims();
if (claims?.sub === undefined) {
throw new Error("The validated ID token did not contain sub");
}
const userinfo = await oidc.fetchUserInfo(
configuration,
tokens.access_token,
claims.sub,
);
await createServerSideApplicationSession({
userKey: claims.sub,
refreshToken: tokens.refresh_token,
});
return userinfo;
};

savePendingSignIn and consumePendingSignIn represent short-lived, server-side storage tied to the browser’s login attempt. Consume the verifier, state, and nonce once at callback time. authorizationCodeGrant validates the authorization response, including the RFC 9207 issuer from discovery and the expected state, as well as the ID-token nonce. The configured non-repudiation check additionally validates the ID-token signature through the discovered JWKS before claims() exposes those claims.

createServerSideApplicationSession represents your application’s own session store. Use the validated, application-scoped sub as the user key, keep any refresh token on the server, and send the browser only your application’s session cookie. Do not serialize the returned token response into that cookie.

OIDC ID tokens are verified against the JWKS at oidc.sudomimus.com/.well-known/jwks.json — that’s the standard OIDC mechanism, and your library does it for you.

The access_token returned by /token does not use the platform-wide OIDC JWKS. It is a per-application access token of the same shape as the Connect flow: payload sub is the pairwise user key, sid is the ApplicationSession, and jti is this token instance. Verify it by kid against https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json. If your OIDC library cannot use a separate JWKS for access tokens, treat the access token as opaque and use /userinfo for claims. See Tokens and verification.