Software

OAuth 2.0 and OpenID Connect Security: Secure Login and API Authorization Patterns

Learn how to implement OAuth 2.0 and OpenID Connect securely with authorization code flow, PKCE, strict redirect URI validation, safe token storage, limited scopes, refresh token protection and practical defenses against common attacks.

Published 4 Oct 2026 · Digital Reality Studio
OAuth 2.0 and OpenID Connect Security: Secure Login and API Authorization Patterns

OAuth 2.0 and OpenID Connect Security: Secure Login and API Authorization Patterns

OAuth 2.0 and OpenID Connect are widely used to connect applications, APIs and identity providers. They can support single sign-on, delegated access and service-to-service authorization, but they are not secure by default. Small implementation details—such as an overly broad redirect URI, an incorrectly validated ID token or an access token stored in an unsafe location—can undermine an otherwise sound architecture.

This guide presents a framework-neutral security approach for modern web applications, single-page applications, mobile apps and APIs. It follows the current OAuth security guidance in RFC 9700, the OAuth 2.0 Security Best Current Practice, together with the core requirements in OpenID Connect Core 1.0 and RFC 7636 for PKCE.

OAuth 2.0 and OpenID Connect are different protocols

OAuth 2.0 is an authorization framework. It allows a client application to obtain limited access to a protected resource, such as an API, on behalf of a resource owner or under its own service identity.

OpenID Connect, usually abbreviated OIDC, is an authentication layer built on OAuth 2.0. It adds a standardized way for a client to verify who authenticated and to receive identity claims in an ID token. OAuth access tokens are intended for APIs. ID tokens are intended for the client that requested authentication. Treating an ID token as an API credential, or treating an access token as proof of a user’s identity, is a common design error.

  • Access token: Presented to a resource server to authorize an API request.
  • ID token: A signed statement about the authentication event and the subject, intended for the OIDC client.
  • Refresh token: Used by an authorized client to obtain a new access token without sending the user through the authorization process again.
  • Authorization code: A short-lived, one-time value exchanged at the token endpoint for tokens.

Before implementation begins, document which system is the authorization server or OpenID Provider, which system is the client, which system is the resource server, and which user or service is granting access. This prevents tokens from being issued to the wrong audience or used for a purpose they were never designed to support.

Use the authorization code flow with PKCE

For interactive login and delegated API access, the preferred pattern is the authorization code flow with Proof Key for Code Exchange, or PKCE. The client redirects the user to the authorization server, receives a short-lived authorization code, and exchanges that code at the token endpoint. The access token is not delivered through the browser’s authorization response URL.

PKCE adds a transaction-specific secret called the code_verifier. The client sends a derived code_challenge in the authorization request and later sends the original verifier during the code exchange. The authorization server issues tokens only if the verifier matches the challenge. This reduces the value of an intercepted authorization code because an attacker that does not possess the verifier cannot redeem it.

Use the S256 challenge method rather than the plain method:

code_challenge = BASE64URL(SHA256(code_verifier))
code_challenge_method = S256

Generate a high-entropy verifier for every authorization transaction. Do not reuse a fixed challenge, store the verifier in a shared location, or place it in a URL that could be logged or disclosed. PKCE is required for public clients under current OAuth security guidance and is also recommended for confidential clients because it helps defend against authorization-code injection and misuse.

A typical authorization request includes:

  • response_type=code
  • A registered client_id
  • An exact, pre-registered redirect_uri
  • A transaction-specific state value
  • A PKCE code_challenge using S256
  • scope=openid when OpenID Connect authentication is required
  • A transaction-specific nonce for OIDC

The implicit grant, which returns an access token directly from the authorization endpoint, exposes tokens to more browser and URL leakage risks. Current guidance recommends using the authorization code response instead.

Validate redirect URIs exactly

Redirect URI validation is one of the most important controls in an OAuth deployment. The authorization server should compare the requested redirect URI with a pre-registered value using exact matching. Avoid wildcard domains, partial string matching and broad patterns such as https://example.com/* unless the provider’s documented security model explicitly makes the pattern safe.

These examples should not be treated as equivalent:

  • https://app.example.com/oauth/callback
  • https://app.example.com/oauth/callback/
  • https://app.example.com/oauth/callback?next=...

Register only the redirect URIs that are required for the specific environment. Keep development and production clients separate where possible. Never let a user-controlled query parameter determine the redirect destination after the OAuth callback. An open redirector can become a path for leaking authorization codes or tokens.

For native applications, use the platform’s recommended redirect mechanism, such as claimed HTTPS links or an application-specific scheme with appropriate protections. Loopback redirects may be appropriate for installed applications when implemented according to the applicable platform guidance.

Protect state, nonce and the authorization transaction

The state parameter binds the authorization response to the browser session and transaction that initiated it. Generate a fresh, unpredictable value for every login attempt, store it server-side or in a securely bound client session, and reject the response if the returned value does not match.

In OpenID Connect, the nonce value binds the authentication request to the ID token. Generate it per transaction and verify that the nonce claim in the received ID token matches the value originally sent. Do not accept a missing, reused or mismatched nonce.

PKCE, state and nonce solve related but distinct problems:

  • PKCE: Protects the authorization code exchange, especially for public clients.
  • State: Helps prevent unsolicited or cross-site authorization responses from being accepted.
  • Nonce: Helps detect replay or substitution of an OIDC ID token.

Use all controls required by the flow rather than assuming one parameter replaces the others. Store transaction records with an expiration time and remove them after successful completion or a failed attempt.

Validate ID tokens correctly

An ID token is normally a signed JWT. Signature verification alone is not sufficient. The client must validate the token’s structure, signature, issuer, audience and time-related claims before establishing a login session.

At minimum, verify:

  • Issuer: The iss claim matches the expected issuer discovered from a trusted configuration.
  • Audience: The aud claim identifies the client that requested the token. If multiple audiences exist, validate azp where required by the OIDC specification.
  • Signature: The token is signed by a trusted key obtained through the provider’s metadata and JWKS endpoints.
  • Expiration: The exp claim has not passed, allowing only a small, documented clock-skew tolerance.
  • Issued-at and timing: Check relevant time claims such as iat and reject implausible values according to the application’s policy.
  • Nonce: The nonce claim matches the value created for the current login transaction.
  • Authentication context: If the application requires a particular authentication method or assurance level, validate the relevant OIDC claims.

Do not accept an arbitrary issuer or JWKS URL supplied by a user or untrusted request parameter. Use provider metadata from a configured, trusted issuer. Also avoid selecting a JWT verification algorithm solely from an untrusted token header without enforcing the algorithms approved by your application.

Store tokens according to the client architecture

Bearer access tokens can be used by whoever possesses them, so token storage is a high-value security decision. The safest approach depends on whether the application is a server-rendered web application, a browser-based single-page application, a native app or a backend service.

Server-side web applications

A backend-for-frontend or traditional server-rendered application can keep access and refresh tokens on the server. The browser receives only a secure application session cookie. Configure that cookie with Secure, HttpOnly and an appropriate SameSite value. Protect state-changing requests against CSRF, and avoid placing OAuth tokens in browser-visible HTML, URLs or client-side logs.

Single-page applications

Browser applications cannot reliably keep long-lived secrets confidential. Use authorization code flow with PKCE and avoid storing long-lived refresh tokens in persistent browser storage unless the identity provider and application architecture provide a carefully reviewed design. A backend-for-frontend can reduce token exposure by keeping tokens server-side and issuing the browser a session cookie.

Be cautious with localStorage and sessionStorage. Any successful script injection or compromised third-party script that can execute in the page may be able to read tokens stored there. Strong Content Security Policy, dependency control, output encoding and secure cookie-based designs reduce this risk.

Native applications

Use the operating system’s secure credential storage where available. Do not embed a client secret in a distributed mobile or desktop application and assume it is confidential. Public native clients should use PKCE and a redirect mechanism appropriate for the platform.

Design scopes and audiences narrowly

Scopes should represent specific capabilities rather than broad access. Prefer scopes such as invoices:read and invoices:write over a single unrestricted scope such as admin. Request only the scopes required for the current operation and verify them at the resource server.

Scopes are not a replacement for authorization policy. The API should still check the authenticated subject, tenant, object ownership, roles and business rules. A token with a valid scope may still be unauthorized to access a particular customer record.

Access tokens should also be audience-restricted. The resource server must verify that the token was intended for that API, using the token’s audience or an equivalent provider-specific mechanism. Do not accept a token issued for one API as a general credential for every service in the environment.

Keep access token lifetimes appropriate to the sensitivity of the data and the client’s ability to refresh. Shorter lifetimes reduce the window for misuse after theft, but they should be combined with reliable refresh and revocation procedures rather than used as the only defense.

Protect refresh tokens

Refresh tokens are valuable because they can provide a continuing path to new access tokens. Keep them out of URLs, analytics events, browser history, application logs and error reports. Encrypt them at rest when stored by a backend, restrict access to the storage system, and limit which application component can use them.

For public clients, current OAuth security guidance requires either sender-constrained refresh tokens or refresh token rotation. With rotation, the authorization server issues a new refresh token whenever the old one is used and invalidates the previous value. If an old token is later presented again, the provider can detect possible replay and revoke the associated token family or session.

Implement refresh failure handling carefully. A rejected refresh token may indicate expiration, user revocation, provider policy or replay. Do not endlessly retry. Clear the local token state, record a security-relevant event without logging the token itself, and require a new authorization transaction when appropriate.

Secure API requests and token handling

Use TLS for authorization, token and API endpoints. Send bearer access tokens in the HTTP Authorization header rather than query parameters. Query parameters are more likely to appear in browser history, reverse-proxy logs, monitoring systems and referrer data.

Authorization: Bearer ACCESS_TOKEN

Resource servers should reject expired, malformed, incorrectly signed or wrongly targeted tokens. They should enforce scopes and application authorization, return appropriate errors without revealing sensitive internals, and avoid reflecting token values in responses or logs.

For higher-risk APIs, consider sender-constrained access tokens using mechanisms such as mutual TLS or Demonstrating Proof of Possession, known as DPoP. These approaches are more complex than ordinary bearer tokens but can reduce the usefulness of a stolen token because the attacker must also demonstrate possession of a bound key.

Common OAuth and OIDC vulnerabilities

  • Open redirectors: An attacker abuses a redirect endpoint that forwards users to arbitrary destinations. Remove open redirects and use exact redirect URI registration.
  • Authorization code injection: A client accepts a code that was not created for the current transaction. Use PKCE, state and transaction binding.
  • Login CSRF: A victim is silently logged into an attacker-controlled account. Validate state and bind the authorization response to the initiating session.
  • Mix-up attacks: A client using multiple providers confuses responses from one issuer with another. Bind the authorization response to the expected issuer and use issuer identification mechanisms supported by the provider.
  • Token leakage through URLs: Tokens appear in fragments, query strings, referrers or logs. Prefer the authorization code flow and keep callback pages free of third-party resources.
  • Weak ID token validation: The application checks only the JWT signature or decodes claims without validating issuer, audience, expiration and nonce.
  • Overbroad scopes: A compromised token grants more access than the application needs. Use narrowly defined scopes and enforce authorization at the API.
  • Client secret exposure: A secret is embedded in JavaScript, a mobile binary or a public repository. Treat browser and distributed clients as public clients.
  • Refresh token replay: A stolen refresh token remains usable indefinitely. Use rotation or sender constraint, expiration policies and revocation.

OAuth implementation and review checklist

  • Use authorization code flow rather than the implicit grant.
  • Require PKCE with the S256 method for public clients and preferably for confidential clients.
  • Generate fresh, unpredictable state, nonce and code verifier values for every transaction.
  • Compare redirect URIs using exact matching and remove unnecessary registered callbacks.
  • Validate issuer, audience, signature, expiration and nonce for every OIDC ID token.
  • Use trusted provider metadata and JWKS configuration rather than user-controlled endpoints.
  • Never treat an ID token as an API access token.
  • Restrict scopes and audiences to the minimum required.
  • Keep access and refresh tokens out of URLs and application logs.
  • Prefer server-side token storage or a backend-for-frontend for browser applications.
  • Use secure, HttpOnly, appropriately SameSite-configured session cookies.
  • Rotate or sender-constrain refresh tokens for public clients.
  • Validate tokens and authorization policy at every resource server.
  • Document revocation, logout, refresh failure and account recovery behavior.
  • Test authorization denial, callback tampering, code reuse, issuer substitution, token replay and scope escalation.

Final recommendations

OAuth security is primarily an exercise in correct boundaries. Use OAuth access tokens for API authorization, use OpenID Connect ID tokens for client-side authentication, and keep the authorization code exchange separate from the application session. Prefer authorization code flow with PKCE, validate every protocol value that influences trust, minimize token privileges and assume that any exposed bearer token may be replayed.

For a broader security baseline covering accounts, email, devices, backups and cloud services, see the Small Business Cybersecurity: Complete Guide for 2026. OAuth should be reviewed as part of that wider identity and access management program, not as an isolated login feature.

Authoritative references