ComplicatedAuth
Menu

Guides / OAuth and OpenID Connect

OAuth and OpenID Connect.

Register a durable client and complete the ComplicatedAuth authorization-code profile with explicit consent, S256 PKCE, pairwise subjects, signed tokens, UserInfo, and revocation.

Current profile: authorization code only, S256 PKCE on every client, RS256 ID and access tokens, openid profile email scopes, pairwise subjects, optional exact Resource Server indicators and delegated scopes, and no refresh tokens. No OAuth access token authorizes ComplicatedAuth management APIs.

Resource boundaries

An OAuth Application is a Tenant-owned client registration, not a Project and not a Project service account. Its immutable type is public or confidential; its client ID is a public opaque identifier; and its redirect URIs are an exact canonical set. Confidential client secrets are separate expiring resources because reusable credential values must never appear in ordinary application reads.

Resource Servers, delegated scopes, and administrative client grants are separate authorization resources. See Delegated authorization before requesting a non-UserInfo audience.

Deployment origins

  • OAUTH_ISSUER is the stable public API origin published in discovery and token iss. Production requires HTTPS. It is configured, not inferred from Host or forwarded headers.
  • CONSOLE_ORIGIN is the stable first-party browser origin used for authentication, consent, CSRF checks, and console cookies.
  • INTERNAL_API_URL is only the console container’s runtime upstream. It is deployment topology and never appears in the public protocol.

Changing an issuer creates a different security domain and invalidates client discovery assumptions. Treat it like a data migration, not a routine environment rename.

Register an application

  1. Open OAuth applications in the console and register a type, name, and one or more exact redirects.
  2. For a confidential client, create an expiring client secret and copy it into a backend secret manager. Public clients never receive a secret.
  3. Retain the representation version or response ETag. Updates and deletion require the current strong If-Match value.
  4. During rotation, create the replacement, deploy it, confirm last_used_at, then revoke the old credential. At most two unexpired active secrets are permitted.

Application and secret creation require Idempotency-Key. Exact retries replay the same client ID or one-time secret response for 24 hours; changed inputs conflict. Deletion tombstones the application and permanently reserves its client ID.

Discover the provider

GET {OAUTH_ISSUER}/.well-known/openid-configuration
GET {OAUTH_ISSUER}/oauth/jwks

Cache discovery and JWKS according to Cache-Control. When a valid token names an unknown kid, refresh JWKS once before rejecting it. Never fetch a JWKS URL supplied by the token itself.

Authorization request

Generate a fresh code verifier, state, and nonce per browser attempt. Store them in the client’s server-side login transaction or a secure same-site session; do not put the verifier in the authorization URL.

const verifier = base64url(randomBytes(32));
const challenge = base64url(sha256(verifier));

const authorize = new URL(discovery.authorization_endpoint);
authorize.search = new URLSearchParams({
  response_type: "code",
  client_id: CLIENT_ID,
  redirect_uri: "https://app.example.com/oauth/callback",
  scope: "openid profile email",
  state,
  nonce,
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();

ComplicatedAuth validates the active client and exact redirect before any redirect occurs. It then creates a hashed ten-minute request handle and places that handle in the console URL fragment, keeping it out of HTTP request lines. The console safely resumes it through login, shows the client, redirect host, scopes, and expiry, and requires an explicit approve or deny decision.

Callback and code exchange

At the callback, require the original state and the configured issuer response parameter before exchanging the code. Reject missing, duplicated, or unexpected parameters.

const body = new URLSearchParams({
  grant_type: "authorization_code",
  code,
  redirect_uri: "https://app.example.com/oauth/callback",
  code_verifier: verifier,
});

const response = await fetch(discovery.token_endpoint, {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    Authorization: "Basic " + Buffer.from(CLIENT_ID + ":" + CLIENT_SECRET).toString("base64"),
  },
  body,
});

Public clients omit Basic authentication and include client_id in the form. The code is five-minute, one-time, and bound to the client, exact redirect, and verifier. If the successful token response is lost after the code is consumed, start a new authorization flow; retrying cannot safely recreate a standard token response.

Validate the ID token

  • Allow only the discovered RS256 algorithm and select the published key by kid.
  • Verify the signature, exact issuer, client ID audience, expiry, issued-at sanity, and the original nonce.
  • Use sub as the external account key. It is stable for one member/application pair and deliberately differs between applications.
  • Read name only with profile and email claims only with email. Do not join accounts solely by email.

Access token, UserInfo, and revocation

Without a resource indicator, the JWT access token is audience-bound to the issuer’s UserInfo endpoint and expires after ten minutes. Call UserInfo with Authorization: Bearer …; the server checks expiry, application and member status, audience, and database revocation before returning scoped claims. Resource-bound tokens are deliberately rejected by UserInfo.

Confidential clients authenticate to /oauth/revoke with Basic; public clients submit client_id. Revocation returns success even for unknown tokens, as required to avoid a token oracle. Tenant Members may also revoke the application grant under My account, which revokes all server-tracked access tokens for that member/application pair.

Failure and retry rules

  • Authorization errors redirect only after exact client and redirect validation. An untrusted redirect receives a local HTML error.
  • Token, UserInfo, and revocation endpoints use the OAuth error/error_description shape rather than the management API envelope.
  • Do not retry invalid grants or client authentication. A code replay is always invalid_grant.
  • Consent decisions are idempotent because they can return one-time codes; reuse their original key only for the identical decision.
  • Never log codes, request handles, verifiers, client secrets, access tokens, ID tokens, or complete callback URLs.

Search guides, architecture, SDKs, and the REST API.