OAuth and OIDC Security Checklist for Web Apps: PKCE, state, nonce, Redirect URIs, and Token Validation

Review OAuth and OIDC web app security with PKCE, exact redirect URIs, state, nonce, issuer checks, token validation, and API authorization.

Back to Blog

OAuth and OIDC Security Checklist for Web Apps: PKCE, state, nonce, Redirect URIs, and Token Validation

OAuth failures rarely come from one dramatic coding mistake. More often the pieces are individually familiar but do not fit together safely: a redirect URI is too broad, PKCE is optional, the application trusts whichever issuer appears in a response, an ID token audience is never checked, or an authorization response can be bound to the wrong browser session.

OAuth 2.0 Security Best Current Practice is now documented in RFC 9700. For teams maintaining web applications in 2026, that document is a better baseline than old tutorials built around permissive redirect matching or the implicit flow. OpenID Connect adds identity-specific checks such as ID token validation and nonce handling.

Know which protocol job you are solving

OAuth is primarily an authorization framework. OpenID Connect adds an identity layer. That distinction matters because an access token and an ID token have different audiences and purposes.

  • Authorization code: a temporary value exchanged for tokens.
  • Access token: presented to an API to authorize access.
  • ID token: an OpenID Connect token containing authentication claims for the client.
  • Refresh token: used to obtain new tokens without repeating the full user interaction, subject to provider policy.

Do not send an ID token to an API just because it is a JWT, and do not treat a decoded access token as user authentication unless the provider and API contract explicitly define that use.

Use Authorization Code with PKCE

PKCE binds the authorization request to the later code exchange. The client creates a high-entropy code verifier, derives a challenge, and sends that challenge in the authorization request. When exchanging the authorization code, it proves possession of the original verifier.

code_verifier = random_high_entropy_value()
code_challenge = BASE64URL(SHA256(code_verifier))

authorization request:
  code_challenge=<derived value>
  code_challenge_method=S256

token request:
  code_verifier=<original value>

RFC 9700 recommends PKCE broadly, not only for native applications. Use the S256 transformation where supported. Do not downgrade to a weaker mode simply because an old integration example did so.

Redirect URIs should be exact

Loose redirect matching turns a convenience feature into an authorization-code theft opportunity. RFC 9700 requires authorization servers to compare redirect URIs using exact string matching, except for the narrow localhost-port behavior allowed for native applications.

A registered URI such as:

https://app.example.com/oauth/callback

should not implicitly authorize:

https://app.example.com/oauth/callback/anything
https://evil.example/?next=https://app.example.com/oauth/callback
https://app.example.com.evil.test/oauth/callback

Keep production, staging, and local development callback URIs explicit. If an identity provider lets you register wildcard callbacks, resist using them unless the protocol and deployment truly require that behavior and you have a well-understood containment model.

Never create an open redirector around the callback

RFC 9700 specifically warns that clients must not expose open redirectors because attackers can use them to exfiltrate authorization responses or tokens. A common pattern is a callback endpoint that accepts ?next= and redirects to any supplied URL after login.

If the application needs post-login navigation, store a server-side return destination before starting the flow or validate the destination against a strict same-origin allowlist. Vulnify's Open Redirect Checker can help assess obvious public redirect behavior, but authorization-flow correctness still needs application-level testing.

Use state to bind the response to the browser session

The state parameter is commonly used to link the authorization response to the browser session that initiated it and to defend against cross-site request forgery. Generate an unpredictable value, store it in session state, and verify it before accepting the response.

Do not put sensitive application data directly in a readable state string. If you need to preserve navigation context, store that context server-side keyed by the random state value, or use an integrity-protected representation designed for that purpose.

Use an OIDC nonce where the flow requires it

OpenID Connect uses a nonce to bind an ID token to the authentication request and reduce replay risk in the relevant flows. The client sends the nonce in the authorization request, stores it, and verifies that the ID token contains the expected value.

Do not substitute state for nonce or vice versa without understanding the provider's protocol. They solve related but different binding problems.

Validate the issuer, especially with multiple authorization servers

Mix-up attacks become relevant when a client can interact with more than one authorization server. RFC 9700 recommends issuer identification and validation so the client does not send a code or credentials to the wrong server.

Configure the issuer explicitly. Validate the issuer identifier in responses and tokens according to the discovery metadata and library behavior. Do not accept an arbitrary iss value and then fetch keys or token endpoints from whatever URL the token names.

Validate ID tokens completely

A JWT decoder is useful for troubleshooting an ID token, but production validation needs more than readable JSON. Check:

  • the signature against keys from the configured issuer;
  • the expected signing algorithm;
  • the issuer claim;
  • the audience and authorized party rules required by the provider;
  • expiration and relevant time claims;
  • the nonce when used;
  • protocol-specific claims required by the selected flow.

Vulnify's JWT / Token Decoder can display alg, kid, iss, aud, exp, nbf, and other claims. It does not have your application's trust configuration and therefore cannot replace server-side verification.

Validate access tokens at the API boundary

An API should verify that a token is intended for that API. Do not accept any token signed by the same identity provider. Check the audience, issuer, expiration, token type, scopes or permissions, and the authorization rules for the requested object.

This is particularly important in microservice environments where several APIs share one identity provider. A valid token for the profile API is not automatically valid for the billing API.

Protect refresh tokens more strongly than short-lived access tokens

Refresh tokens can extend a session far beyond one access-token lifetime. Keep them out of browser-readable storage where the architecture allows, bind them to the client, rotate them according to provider capabilities, and revoke them when the session or account changes in a security-relevant way.

For browser-based applications, review current provider guidance rather than copying a decade-old localStorage example. The correct storage pattern depends on whether you use a backend-for-frontend, same-site cookies, token binding features, and the provider's supported flow.

Do not pretend a browser can keep a client secret

A secret embedded in JavaScript, a mobile binary, or a downloadable desktop application is not a confidential client secret. Public clients must rely on controls designed for public-client environments, including PKCE and exact redirect handling.

Server-side confidential clients should protect credentials in a secret-management system and rotate them when needed.

Authentication success is not authorization

OIDC can tell an application that the identity provider authenticated a subject. It does not decide whether that subject may view another tenant's invoice, change an administrator setting, or export customer data.

Authorization must be enforced at the resource and action level inside the application. This is why a clean OAuth integration can still have broken access control.

OAuth and OIDC deployment checklist

  • Use Authorization Code with PKCE for interactive client flows where supported.
  • Use S256 for the PKCE challenge.
  • Register exact redirect URIs.
  • Do not expose open redirectors in client callbacks or post-login navigation.
  • Generate and validate unpredictable state.
  • Generate and validate OIDC nonce where required.
  • Pin and validate the expected issuer.
  • Verify JWT signatures using trusted issuer keys.
  • Validate audience, expiry, token type, and required claims.
  • Enforce scopes and object-level authorization at APIs.
  • Protect refresh tokens and client credentials appropriately.
  • Use HTTPS across authorization, callback, and token endpoints.
  • Test production and staging callback configurations separately.
  • Monitor changes to identity-provider configuration and signing keys.

Public checks that complement OAuth testing

Vulnify can support focused checks around an OAuth-enabled public application. The Open Redirect Checker is relevant to redirect handling, the JWT / Token Decoder helps inspect token claims during authorized troubleshooting, and the SSL/TLS tools verify the public transport layer. A broader website scan can assess the surrounding public application within Vulnify's supported scan scope.

Those tools cannot prove that every authorization-code exchange, nonce comparison, issuer check, or resource-level authorization decision is correct. Protocol integration tests and application security tests are still necessary.

Keep authorization codes and tokens out of logs and unintended URLs

OAuth credentials can leak through places that were never designed as secret stores. Avoid putting access tokens in query strings, where they can appear in browser history, reverse-proxy logs, referrer data, analytics systems, and support screenshots. Use the protocol's intended headers and back-channel exchanges.

Authorization codes are short-lived, but they still deserve careful handling. PKCE reduces the value of a stolen code when the attacker lacks the verifier, yet logging the code and verifier together would defeat that protection. Redact both from routine application logs.

Review error handling too. An OAuth callback should not print the complete token response or provider secret into a browser error page. Log a correlation identifier and the minimum diagnostic detail necessary to investigate the failure.

The useful mental model

An OAuth flow is a chain of bindings. The browser session is bound to the authorization response. The code is bound to the client and PKCE verifier. Tokens are bound to a trusted issuer and intended audience. API actions are bound to scopes, roles, tenants, and objects.

Security problems appear when one of those bindings becomes "any value is acceptable." Exact redirect URIs, PKCE, state, nonce, issuer validation, and complete token verification exist to prevent that ambiguity.