Skip to content
All articles
Identity & AccessOct 5, 20266 min read

OAuth & OIDC: validate the whole sign-in transaction

A valid signature is only one check. Bind the authorization response, tokens and local session to the same intended login.

By ProteQon Research

The boundary is a transaction, not a token

OAuth 2.0 delegates access; OpenID Connect adds an identity layer. A browser returning to a callback URL has not, by itself, proved who should be signed in. The application must establish that the response belongs to a login it initiated, that the identity provider is the expected one, and that the resulting identity is allowed to create the intended local session. A cryptographically valid token can still be the wrong token for this application.

Begin by drawing the actual flow: browser, application, authorization endpoint, token endpoint and session store. Record the client type, registered redirect URI, configured issuer, requested scopes and local account-linking rule. Check web, mobile and administrative clients separately. Shared identity infrastructure does not mean their redirect URIs, audiences or session boundaries are interchangeable.

Bind the redirect to the login that initiated it

Use the authorization-code flow with PKCE and the S256 challenge method. Generate a fresh verifier for each transaction and keep it bound to that transaction. Public clients must use PKCE under the OAuth security best current practice; confidential clients also benefit from it. A client secret is not a replacement for validating the browser-facing part of the flow.

Use an established OAuth/OIDC library to bind anti-CSRF protection and the response to the initiating browser session. A state value, when used, needs unpredictability, expiration, single-use handling and a session association; merely comparing it with another attacker-controlled value does not establish that binding. If an OIDC nonce was sent, verify that the ID token contains the expected nonce. Do not remove state or nonce checks simply because a token has a valid signature.

Register precise callback URLs and reject unexpected redirect URIs. RFC 9700 requires exact redirect-URI matching, with the specified exception for native-app loopback ports. Treat a post-login return destination as a separate input: constrain it to approved local destinations instead of allowing an open redirect. An application supporting multiple issuers must also defend against authorization-server mix-up; never choose a token endpoint from an untrusted callback parameter.

Validate the ID token in its intended context

  • Verify the signature with an appropriate, explicitly allowed algorithm and keys obtained from the configured, trusted issuer. Do not follow an arbitrary key URL supplied by the token.
  • Require the expected issuer (iss) and the client ID in the audience (aud). Apply the OIDC authorized-party (azp) checks when relevant; another application’s ID token is not a login credential for yours.
  • Reject expired tokens. Apply a small, deliberate clock-skew allowance, and validate other time-related claims according to the protocol and library policy.
  • Check the nonce against the login transaction when one was sent. Reject a response whose transaction is missing, expired, already consumed or associated with another browser session.
  • Identify a federated account using the issuer-and-subject pair. Treat linking by email as a separate security decision, not an automatic consequence of receiving a matching string.

An ID token describes an authentication event for a client; an access token authorizes access to a resource server. Do not interchange them. The API must validate its own intended audience, scopes and other authorization conditions, using token introspection or local verification as appropriate to the provider and token format. Decoding a JWT payload is inspection, not validation.

An illustrative server-side validation boundary

typescript
// Pseudocode: use your OIDC library's documented callback API.
// Transaction lookup must be session-bound, expiring and single-use.
const transaction = await consumeLoginTransaction(
  browserSession,
  callback.state
);

const tokens = await oidcClient.exchangeCode({
  code: callback.code,
  redirectUri: REGISTERED_REDIRECT_URI,
  codeVerifier: transaction.pkceVerifier,
});

const identity = await oidcClient.verifyIdToken(tokens.id_token, {
  issuer: CONFIGURED_ISSUER,
  audience: CLIENT_ID,
  nonce: transaction.nonce,
  allowedAlgorithms: CONFIGURED_ALGORITHMS,
});

// Only validated claims reach the account/session boundary.
await establishSession({
  issuer: identity.iss,
  subject: identity.sub,
});

These helper names are illustrative, not a drop-in SDK. The important property is the boundary: unvalidated claims cannot reach account creation, account linking or session establishment. Failures should return a controlled sign-in error, not continue with whichever claims were available. A retry after a consumed transaction should start a new login rather than resurrecting a used authorization response.

Negative tests that produce useful evidence

In a test tenant, compare a successful baseline with one deliberately invalid condition at a time: a missing or changed state, an incorrect PKCE verifier, a reused code, a mismatched nonce, a token for a second authorized test client, an expired token, or a token from a different test issuer. Separate callback validation failures from account-policy rejections. The expected result is no new local session and no account mutation; a friendly error screen alone does not prove that.

  • Capture the configured issuer, client type, redirect URI and expected audience without recording client secrets.
  • Record the rejected condition and observable session/account outcome. Redact authorization codes, tokens, cookies and personal claims from evidence.
  • Test concurrent login tabs, canceled provider consent and expired transactions so fixes do not introduce accidental account linking or unsafe retry paths.
  • After remediation, repeat the negative cases and a legitimate login. Confirm that logs are useful for diagnosis without containing credentials.

References

  1. 01IETF RFC 9700 — Best Current Practice for OAuth 2.0 Security
  2. 02OpenID Connect Core 1.0 — ID Token Validation
  3. 03IETF RFC 7636 — Proof Key for Code Exchange

Start an engagement

Tell us what you're shipping.
We'll tell you what it's exposing.

Book a 30-minute scoping call with a senior engineer. You'll leave with a clear recommendation on scope, approach, and timeline.