Skip to content

OAuth And OIDC

Use this reference after Add SigID Login when you need to move from a first working login to a correct production integration. It explains OAuth behavior, token safety, grant selection, scopes, consent, tenant isolation, and the common mistakes that break real deployments.

If you are preparing the tenant, application, users, and handoff values for a development team, start with Launch Your Workspace.

Who this page is for

This page is for application developers and backend API owners who already have or are about to receive a SigID application configuration.

By the end of this page, you should know how to:

  • run Authorization Code with PKCE without leaking secrets
  • choose the right grant type for your app
  • exchange authorization codes with the registered client auth method
  • validate access tokens before serving protected resources
  • use tenant-scoped subjects instead of email addresses as user keys
  • request scopes and refresh tokens deliberately
  • troubleshoot OAuth error responses without guessing

On This Page

Before you start

Get these values from your tenant administrator or platform team. They should match the handoff packet from Launch Your Workspace.

Value Used for
SIGID_ISSUER_URL Discovery, hosted auth redirects, token exchange, JWKS, and /userinfo
Tenant ID or tenant slug Support, logs, tenant isolation, and environment troubleshooting
SIGID_CLIENT_ID Authorization requests and public-client token exchange
SIGID_REDIRECT_URI Callback allowlist and authorization-code token exchange
SIGID_SCOPES Consent, UserInfo claims, and API authorization checks
Token endpoint auth method Whether token exchange uses none, client_secret_post, client_secret_basic, or configured private_key_jwt
Client secret Confidential backend clients only; never expose this to browsers or mobile apps
API audience Required when your backend validates access tokens for an API
Refresh-token policy Whether the app may request offline_access and use the refresh_token grant
Test user A non-admin user that can exercise the expected tenant, organization, role, and policy state

Keep development, staging, and production clients separate. Redirect URIs, secrets, scopes, and audiences should not drift between environments by accident.

Choose the right OAuth path

Use case Recommended path Notes
Web, mobile, desktop, or browser login Authorization Code with PKCE Default path for interactive user login
Web app with a trusted backend Authorization Code with PKCE plus a confidential client auth method Keep the client secret server-side only
Browser-only, mobile, desktop, or CLI app without a trusted secret store Authorization Code with PKCE as a public client Use token endpoint auth none; still use PKCE
Backend service calling another backend Client Credentials Confidential clients only; no refresh token is issued
CLI, TV, terminal, or input-constrained device Device Authorization Requires the device-code grant and platform support
Agent acting for a user Token Exchange Validate subject_type, act, audience, scopes, and delegation policy
Backchannel approval without browser redirect CIBA Use only when the app has a real backchannel-auth use case
Long-lived user session Authorization Code with PKCE plus offline_access Request only when the app needs refresh tokens

Most applications should start with Authorization Code with PKCE and add other grants only when there is a concrete product workflow.

Authorization Code with PKCE

Authorization Code with PKCE is the recommended flow for user login. SigID signs the user in on hosted auth, redirects back with an authorization code, and your app exchanges that code at /oauth/token.

1. Create state and PKCE values

Create a random state value for CSRF protection and a random PKCE code_verifier. Send only the derived code_challenge in the authorization request. Store the original state and code_verifier in a server-side session or another storage location that is bound to the browser interaction.

import crypto from "node:crypto";

function base64url(bytes) {
  return Buffer.from(bytes).toString("base64url");
}

export function createPkceRequestState() {
  const state = base64url(crypto.randomBytes(32));
  const codeVerifier = base64url(crypto.randomBytes(32));
  const codeChallenge = base64url(
    crypto.createHash("sha256").update(codeVerifier).digest(),
  );

  return { state, codeVerifier, codeChallenge };
}

SigID supports the S256 PKCE method. Do not send the verifier in the authorization request.

2. Redirect to hosted auth

Build an authorization URL on the issuer:

GET /oauth/authorize
  ?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback
  &scope=openid%20profile%20email
  &code_challenge=BASE64URL_SHA256_VERIFIER
  &code_challenge_method=S256
  &state=OPAQUE_CSRF_VALUE

Use the exact registered redirect URI. These are different values:

https://app.example.com/auth/callback
https://app.example.com/auth/callback/
http://app.example.com/auth/callback

3. Handle the callback

Successful callback:

GET https://app.example.com/auth/callback?code=AUTH_CODE&state=OPAQUE_CSRF_VALUE

Error callback:

GET https://app.example.com/auth/callback?error=access_denied&error_description=...

Callback handler checklist:

  • verify the returned state
  • handle error as a user-visible retry path
  • exchange each code only once
  • use the same redirect_uri used in the authorization request
  • use the original code_verifier
  • do not log codes, tokens, client secrets, or refresh tokens

4. Exchange the authorization code

Use the token endpoint auth method registered on the application. Do not infer the method from the runtime. Check the application settings or dynamic registration response and use the exact registered value.

Public PKCE client:

curl -sS "$SIGID_ISSUER_URL/oauth/token" \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=$SIGID_CLIENT_ID" \
  --data-urlencode "code=$AUTHORIZATION_CODE" \
  --data-urlencode "redirect_uri=$SIGID_REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

Confidential client using client_secret_post:

curl -sS "$SIGID_ISSUER_URL/oauth/token" \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=$SIGID_CLIENT_ID" \
  --data-urlencode "client_secret=$SIGID_CLIENT_SECRET" \
  --data-urlencode "code=$AUTHORIZATION_CODE" \
  --data-urlencode "redirect_uri=$SIGID_REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

Confidential client using client_secret_basic:

curl -sS "$SIGID_ISSUER_URL/oauth/token" \
  -u "$SIGID_CLIENT_ID:$SIGID_CLIENT_SECRET" \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$AUTHORIZATION_CODE" \
  --data-urlencode "redirect_uri=$SIGID_REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

Typical response without refresh-token access:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "openid profile email",
  "id_token": "eyJ..."
}

For user login flows such as Authorization Code, Device Authorization, and CIBA, SigID returns a refresh_token only when offline_access was granted. Delegated token exchange can issue a new refresh-token family for the actor according to delegation policy.

5. Create your application session

After token exchange, your backend should either validate access tokens on every API request or create a trusted server-side application session after validation. Do not authorize protected API requests from frontend login state alone.

Token validation

Access tokens protect APIs. ID tokens describe the authentication result for the client, and UserInfo returns consented profile claims. Do not use an ID token, UserInfo response, or decoded JWT payload as a substitute for access-token validation.

Use the checklist below as the minimum validation contract for protected APIs. Every protected API request should fail closed unless these checks pass:

Check What to verify
Discovery and JWKS Load issuer metadata from /.well-known/openid-configuration and signing keys from the discovered JWKS URI
Token type and signature Verify the JWT signature with the expected algorithm and key; do not only decode the payload
Issuer iss matches SIGID_ISSUER_URL
Audience aud matches your API audience, not some other client or service
Lifetime exp and nbf allow the token at request time
Tenant tenant_id matches the tenant that owns the requested resource
Scope scope contains the permission required for the action
Subject type subject_type is acceptable for the route, such as human, agent, system, or anonymous
Delegation If act is present, enforce the delegated actor policy and reduced scopes
Sender constraint If DPoP is used, verify the DPoP proof and cnf binding

A minimal backend authorization pattern is:

  1. Read Authorization: Bearer <access_token>.
  2. Verify the access token with the discovered JWKS and expected issuer.
  3. Require your API audience.
  4. Match tenant_id to the resource tenant before checking scopes.
  5. Require the route's scope or policy.
  6. Reject unexpected subject types, agent claims, or delegation chains.
  7. Build your application principal from validated claims only.

Tenant-scoped subjects and claims

SigID can present pairwise subject identifiers for tenant-scoped third-party user or agent tokens. First-party targets, system principals, and some client-credentials application principals may receive non-pairwise subjects. Use the validated sub claim together with the tenant context as your durable application user key.

Do not use email as a primary key. Email can change, can be unverified in some contexts, and may not uniquely identify the authorization subject.

Do not assume the same human has the same sub across tenants or unrelated client sectors. If your product intentionally shares identity across multiple applications, make that mapping explicit in your own data model.

Important access-token claims:

Claim Use
sub Tenant-application subject identifier for the caller
tenant_id Tenant isolation boundary
client_id OAuth application that received the token
aud API or resource server audience
scope Space-delimited permissions
subject_type Caller category, such as human, agent, system, or anonymous
act Delegated actor chain for token-exchange flows
org Active organization context, when present
cnf DPoP confirmation claim, when sender-constrained

Call /userinfo only for consented human OIDC profile claims:

curl -sS "$SIGID_ISSUER_URL/userinfo" \
  -H "authorization: Bearer $ACCESS_TOKEN"

Your API should authorize from validated access-token claims, not from the UserInfo response.

OAuth and OIDC endpoint reference

Use discovery instead of hard-coding endpoint URLs:

curl -sS "$SIGID_ISSUER_URL/.well-known/openid-configuration"

Common fields:

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/oauth/authorize",
  "token_endpoint": "https://auth.example.com/oauth/token",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "userinfo_endpoint": "https://auth.example.com/userinfo"
}

Endpoint availability can vary by deployment and application policy. Discovery metadata and the target deployment's OpenAPI document are the source of truth for enabled protocol surfaces.

Endpoint Use
GET /.well-known/openid-configuration Discover OAuth and OIDC metadata for the issuer
GET /.well-known/jwks.json Fetch signing keys for JWT verification
GET /oauth/authorize Start hosted login, consent, and authorization-code issuance
POST /oauth/token Exchange authorization codes, refresh tokens, device codes, CIBA grants, client credentials, and token-exchange requests
GET /userinfo Read consented OIDC profile claims for a human user
POST /oauth/revoke Revoke refresh tokens or access tokens during logout, disconnect, or compromise handling
POST /oauth/introspect Let trusted backends inspect token state when local JWT validation is not enough
POST /oauth/par Push large or sensitive authorization request parameters before redirecting
POST /oauth/device/code Start Device Authorization for input-constrained clients
GET /oauth/device/verify User-facing device verification page
POST /bc-authorize Start CIBA backchannel authentication
GET /api/v1/identity/ciba/requests List pending CIBA requests for the authenticated human user
POST /api/v1/identity/ciba/requests/{id}/approve Approve a pending CIBA request
POST /api/v1/identity/ciba/requests/{id}/deny Deny a pending CIBA request
GET or POST /oauth/end-session OIDC RP-initiated logout, when enabled
POST /oauth/register Dynamic Client Registration, when enabled

For the full product surface, see Product Reference.

Choose a grant type

Grant Use it for Avoid when
authorization_code Interactive user login with PKCE The caller is a backend service with no user
refresh_token Renewing a user session after offline_access was granted The app only needs a short browser session
client_credentials Machine-to-machine backend access Public clients or user-delegated actions
urn:ietf:params:oauth:grant-type:device_code CLI, TV, terminal, and other input-constrained clients A normal browser redirect is available
urn:ietf:params:oauth:grant-type:token-exchange Agent or delegated access after policy checks You are only logging in a human user
urn:openid:params:grant-type:ciba Backchannel user approval Your app can use a normal redirect flow

Every grant must be allowed on the application. If a request uses a known grant that the application does not allow, the token endpoint returns unauthorized_client. If the grant_type value itself is unsupported or malformed, the token endpoint returns unsupported_grant_type.

Start with the smallest scope set that proves the login flow. Add broader scopes only when the user takes a feature-specific action that needs them.

Common scopes:

Scope Meaning
openid Request OIDC login semantics and an ID token
profile Allow profile claims such as name, when available
email Allow email claims, when available
offline_access Allow refresh-token issuance when policy and grant configuration permit it
API scopes such as projects:read Authorize your own protected API actions

Consent text should describe the user-visible result, not only the internal scope name. For delegated agent access, explain what the agent can do, whose authority it uses, and when that access ends.

Advanced authorization parameters:

Need Parameter or endpoint
Force login prompt=login
Force consent prompt=consent
Silent check with an existing session prompt=none
Require recent authentication max_age=300
Request step-up assurance acr_values=urn:sigid:aal2
Request specific OIDC claims claims
Request rich authorization details authorization_details
Push request parameters before redirect POST /oauth/par, then authorize with request_uri

Advanced parameters and endpoints must be enabled by the deployment and allowed for the application before clients depend on them.

Refresh tokens and revocation

Request offline_access only when your application needs to renew a user session without another browser redirect.

Refresh-token rules:

  • for user login flows, no offline_access scope means no refresh_token in the token response
  • delegated token exchange may return a refresh token according to delegation policy
  • store refresh tokens server-side or in platform-secure storage
  • keep refresh tokens out of logs, analytics, screenshots, and support tickets
  • persist the newest refresh token if rotation returns one
  • do not use refresh tokens for client_credentials
  • treat refresh-token reuse as a security event
  • revoke tokens on logout, disconnect, account removal, or suspected compromise

Refresh with a public client:

curl -sS "$SIGID_ISSUER_URL/oauth/token" \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "client_id=$SIGID_CLIENT_ID" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN"

For a confidential client, authenticate with the same token endpoint auth method registered on the application.

Device authorization, CIBA, and token exchange

Use Device Authorization for input-constrained clients:

curl -sS "$SIGID_ISSUER_URL/oauth/device/code" \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=$SIGID_CLIENT_ID" \
  --data-urlencode "scope=openid profile"

The client shows the returned user code and polls /oauth/token according to the returned interval. Respect authorization_pending, slow_down, access_denied, and expired_token.

Use CIBA only when the client cannot redirect the user but can start a trusted backchannel authorization request. SigID supports poll delivery mode with confidential clients. POST /bc-authorize accepts scope, login_hint, optional binding_message, optional requested_expiry, and optional authorization_details/claims; it rejects login_hint_token, id_token_hint, push/ping notification parameters, CIBA user_code, and CIBA-specific acr_values. Unknown or inactive login hints return the same successful start response as known users, but create a decoy request that never appears in the user's approval list and never issues tokens. The authenticated human lists pending requests at /api/v1/identity/ciba/requests, approves or denies one by ID, and the client polls /oauth/token with grant type urn:openid:params:grant-type:ciba and auth_req_id. Pollers must honor authorization_pending, slow_down with Retry-After, access_denied, and expired_token.

Use Token Exchange for delegated access, especially agent workflows. Do not treat delegated agent tokens as ordinary human user tokens. Validate audience, tenant, reduced scopes, subject_type, and act claims before allowing tool or API actions. For agent-specific guidance, see Agent And MCP Auth.

Hosted sign-in methods

Applications should redirect users to SigID hosted auth. Do not reimplement password, passkey, social login, SSO, MFA, recovery, or consent screens inside your application unless a product-specific integration explicitly requires it.

Tenant administrators decide which hosted sign-in methods are enabled and how Enterprise SSO domains route users. Developers should handle OAuth redirects, callbacks, token exchange, token validation, and application sessions.

Common integration mistakes

Mistake Fix
Using SIGID_AUTH_ORIGIN in new code Use SIGID_ISSUER_URL
Redirect URI mismatch Copy the exact registered URI, including scheme, host, port, path, and trailing slash
State mismatch Store state server-side and reject mismatches
PKCE mismatch Use the original code_verifier for token exchange
Token auth method mismatch Use the registered method: none, client_secret_post, client_secret_basic, or configured private_key_jwt
Expecting refresh_token from user login without offline_access Request offline_access only when the app needs refresh tokens
Application does not allow the grant Enable the grant or use the correct app; the error is unauthorized_client
Unsupported grant_type value Fix the literal grant value; the error is unsupported_grant_type
Accepting any audience Require your API audience
Decoding JWTs without verifying them Verify signature, issuer, audience, lifetime, and tenant
Trusting frontend-only login state Validate access tokens on the backend or create a server-side session after validation
Using email as user ID Store the tenant-scoped sub with tenant context
Treating agents as human users Check subject_type, agent, and act claims
Mixing tenant or environment values Keep issuer, tenant, client ID, redirect URI, and audience from the same environment
Ignoring callback errors Show a retry path and log a request ID without logging secrets

Next steps