OAuth 2.0 and OpenID Connect: Common Implementation Mistakes
OAuth 2.0 allows applications to access resources on behalf of users without requiring them to share their passwords with every service they use. OpenID Connect builds on OAuth to support user authentication. Both are widely used, but a secure integration depends on how the protocols are implemented, how tokens are handled, and how the application validates responses from the authorization server.
A login button can work perfectly during ordinary testing while important security checks are missing. A callback might accept an unexpected authorization response, a redirect configuration might send a code to an unintended destination, or a stolen access token might provide more access than the application actually needs.
This guide examines common OAuth and OpenID Connect implementation weaknesses, explains the risks without overstating them, and provides practical guidance for developers and security reviewers. It focuses on defensive implementation, testing, and configuration rather than exploiting live services.
In this post
- OAuth 2.0 vs OpenID Connect
- Missing or incorrectly validated state
- Redirect URI validation
- Token leakage and unsafe URL handling
- Choosing the correct OAuth flow
- How PKCE protects authorization codes
- Validating OpenID Connect ID tokens
- Excessive scopes and permissions
- Token storage and refresh-token security
- A practical security checklist
1. OAuth 2.0 vs OpenID Connect: understanding the difference
One of the most important distinctions in this area is the difference between authorization and authentication.
OAuth 2.0 is an authorization framework. It allows a client application to obtain limited access to a protected resource, subject to the authorization server's policies and the permissions granted. For example, a user might authorize a calendar application to read calendar events without giving that application their account password.
OpenID Connect (OIDC) is an identity layer built on OAuth 2.0. It defines how a client can authenticate a user and obtain standardized identity claims. A central component is the ID token, which contains claims about the authentication event and the user, and is issued by the OpenID Provider.
This distinction matters when implementing a feature such as "Sign in with Google." An access token is intended to authorize access to a resource. It should not automatically be treated as proof of a user's identity by an unrelated application.
Why an access token is not a substitute for an ID token
Access tokens are intended for the resource server that accepts them. Their format and meaning are determined by the authorization system and resource server. Some access tokens are opaque strings; others use a structured format. A client should not assume that every access token is a JWT or that its contents can safely be used to identify a user.
OIDC defines a separate ID token for the authentication use case. A client must validate the token's signature and relevant claims, including the issuer, audience and expiration. Additional checks depend on the flow and the request, and may include nonce validation.
Practical rule: Use OpenID Connect when your application needs to authenticate users. Use OAuth to obtain authorization to access resources. If both are needed, implement the identity and authorization parts according to their respective specifications.
Read the OpenID Connect Core specification for the authentication protocol and ID-token validation requirements.
2. Missing or incorrectly validated state
The state parameter is commonly used to bind an OAuth authorization request to the corresponding response. The client generates an unpredictable value, associates it with the user's browser session or transaction, and sends it with the authorization request.
When the authorization server redirects the user back to the client, the client checks the returned value against the value stored for that transaction. If the values do not match, the application should reject the response rather than proceed with the authorization flow.
This check helps defend against cross-site request forgery and authorization-response injection. Without appropriate request binding, an attacker may be able to cause a victim's browser to process an authorization response that was initiated in a different context. Depending on the application, this can contribute to account-linking attacks, login confusion or other session-related security problems.
Common implementation mistakes
- Not generating a state value for a flow that relies on it.
- Generating a value but failing to validate the returned value.
- Using a predictable value that an attacker can guess.
- Storing one global state value shared by unrelated users or concurrent login attempts.
- Accepting a callback without verifying that it belongs to an active authorization transaction.
Generating a random value is only part of the solution. The application must preserve the association between the value and the transaction, verify it at the correct point, and reject mismatches.
Example of a defensive state check
// Illustrative server-side pseudocode.
// The session store must be protected and correctly configured.
const expectedState = req.session.oauthState;
const returnedState = req.query.state;
if (
typeof expectedState !== "string" ||
typeof returnedState !== "string" ||
returnedState !== expectedState
) {
return res.status(400).send("Invalid authorization response");
}
// Consume the state so it cannot be reused for another callback.
delete req.session.oauthState;
// Continue processing the authorization response only after
// validating the transaction and other required parameters.
This example illustrates the comparison; it is not a complete OAuth callback implementation. A production system also needs secure session handling, error processing, appropriate state generation, and protection against concurrent transactions. Use a maintained OAuth library rather than treating this snippet as a drop-in authentication system.
For browser-based clients, the precise combination of state, session binding and PKCE depends on the client architecture. PKCE also provides important authorization-code protections, but it should not be treated as a reason to ignore transaction validation or OpenID Connect's separate nonce requirements.
3. Improper redirect URI validation
The redirect URI determines where an authorization server sends the browser after the authorization step. In the authorization code flow, the response normally includes a short-lived authorization code that the client later exchanges for tokens.
If the authorization server accepts an unintended redirect destination, an authorization response may reach a location that should never have received it. Whether that results in a successful attack depends on the flow, the attacker's ability to control the destination and other protections, including PKCE.
Why loose matching is dangerous
Suppose a client has registered a callback at https://app.example.com/oauth/callback. A dangerous implementation might accept any address that merely contains the string app.example.com, or any address beginning with a superficially similar prefix.
String-based checks like these can confuse a trusted hostname with an attacker-controlled hostname, path or query parameter. Open redirects on trusted domains can also create problems when authorization codes or other sensitive responses are delivered through the redirect chain.
For example, checking whether a requested URI contains the text example.com is not equivalent to validating the actual scheme, hostname, port, path and registered URI.
How to implement redirect validation
- Register specific callback URIs for each client.
- Use exact matching against registered values where required by the applicable OAuth specification.
- Do not rely on substring matching, loose prefix matching or unreviewed wildcards.
- Do not let arbitrary user-supplied URLs determine where authorization responses are sent.
- Review open redirects and forwarding endpoints that might allow an authorization response to reach an unintended destination.
- Use HTTPS for web callbacks in production, following the applicable specification and provider requirements.
Redirect URI rules have some defined exceptions and special cases, including certain native-app redirect patterns. Implementations should follow the requirements for the specific client type rather than inventing their own generic URL-matching rules.
See RFC 6749, Section 3.1.2, and the more recent OAuth 2.0 Security Best Current Practice.
4. Token leakage and unsafe URL handling
Access tokens, refresh tokens and ID tokens are sensitive credentials. If an attacker obtains a usable token, the attacker may be able to access protected resources or impersonate a session, depending on the token type, permissions, lifetime and server-side controls.
Token leakage can happen through several channels, including browser history, application logs, analytics systems, crash reports, unsafe browser storage, compromised scripts and improperly configured redirects.
Why tokens in URLs are risky
Putting a bearer token in a URL query string is dangerous because query strings may be recorded in web server logs, monitoring systems, browser history and other systems that handle URLs. If a URL containing a token is copied, shared or captured, the token may be exposed to parties that should never receive it.
The OAuth implicit flow historically returned access tokens in the URL fragment. A fragment is handled by the browser and is not normally sent as part of an HTTP request or included in the HTTP Referer header. However, fragment-based token delivery still has risks, including exposure to scripts running in the page, browser-side handling mistakes, and other client-side leaks.
This distinction is important: it is inaccurate to say that every fragment-based token is automatically sent to third parties through Referer headers. Query strings and fragments have different browser behavior, although both can create security problems when handled carelessly.
How to reduce token leakage
- Prefer the authorization code flow with PKCE rather than a flow that delivers access tokens directly through the browser URL.
- Avoid putting access tokens, refresh tokens or ID tokens in query parameters.
- Keep authorization codes short-lived and single-use, and bind their redemption to the correct client and redirect URI.
- Use an appropriate
Referrer-Policyheader, such asstrict-origin-when-cross-origin, where compatible with the application's requirements. - Remove sensitive query parameters from application logs and monitoring pipelines.
- Do not include tokens in analytics events, error messages, support screenshots or URLs shared with third parties.
- Limit third-party scripts on pages that handle authentication responses.
A restrictive referrer policy is a supplementary control, not a substitute for avoiding token exposure. The safest design is to minimize where credentials appear and how many components can access them.
5. Choosing the correct OAuth flow
OAuth defines different authorization grants for different client architectures. Choosing the wrong grant can create unnecessary exposure or encourage an insecure way of handling credentials.
Authorization code flow
In this flow, the authorization server returns a short-lived authorization code to the registered redirect URI. The client exchanges the code for tokens through the token endpoint. The exchange is subject to client authentication where required and other protocol checks.
The authorization code flow is the recommended starting point for most interactive applications. Public clients, including native mobile applications and browser-based applications, should use PKCE.
Implicit flow
The implicit flow was designed for older browser-based applications that received access tokens directly through the authorization response. Modern security guidance advises against using it for new implementations because the authorization code flow with PKCE provides a better security model.
Applications that still depend on implicit flow should evaluate a migration to authorization code with PKCE. Migration requires checking provider support, token handling, redirect configuration and the application's session architecture.
Resource owner password credentials grant
This legacy grant allows a client to collect a user's password and exchange it directly for tokens. It exposes the user's credentials to the client and is incompatible with many modern authentication approaches.
The current OAuth security best practice states that this grant must not be used. Applications should migrate to an appropriate authorization flow instead.
Client credentials grant
The client credentials grant is intended for machine-to-machine authorization where the client acts on its own behalf rather than representing a user through an interactive login. It is not a replacement for a user login flow.
| Grant or flow | Typical use | Security guidance |
|---|---|---|
| Authorization code with PKCE | Web, mobile and browser-based interactive clients | Recommended for modern interactive authorization |
| Authorization code with client authentication | Confidential server-side applications | Use secure client authentication and PKCE where applicable |
| Implicit flow | Legacy browser-based integrations | Avoid for new implementations; migrate where feasible |
| Password grant | Legacy designs that collect user passwords | Must not be used under current OAuth security best practice |
| Client credentials | Machine-to-machine access | Use only when the client acts on its own behalf |
These categories are not a complete list of OAuth extensions. The correct design depends on the type of client, the authorization server and the resources being accessed.
6. How PKCE protects authorization codes
PKCE stands for Proof Key for Code Exchange and is pronounced "pixy." It protects the authorization code flow against attacks in which an attacker obtains an authorization code but cannot legitimately complete the intended client's token exchange.
PKCE works by binding the authorization request to a secret value that the client retains locally for the transaction. The client generates a high-entropy code_verifier and derives a code_challenge from it. The authorization request carries the challenge. When exchanging the returned code for tokens, the client supplies the verifier.
The authorization server verifies that the supplied verifier corresponds to the challenge associated with the original authorization request. An attacker who obtains only the authorization code should not be able to redeem it without the correct verifier.
How the process works
- The client generates a fresh, unpredictable code verifier for the authorization transaction.
- The client derives an S256 code challenge from the verifier.
- The authorization request includes the challenge and the
S256method. - The authorization server associates the challenge with the authorization code.
- The client sends the verifier to the token endpoint when redeeming the code.
- The authorization server checks the verifier before issuing tokens.
For the challenge method, clients should use S256. The verifier should be generated according to the specification rather than constructed from predictable values such as a timestamp or username.
Why use PKCE even on a server-side application? PKCE adds protection against authorization-code interception and injection. It is recommended by modern OAuth security guidance for confidential clients as well as public clients. It complements client authentication rather than replacing it.
PKCE does not replace every other security check. Clients must still validate the authorization transaction, use secure redirect handling, protect tokens and perform the relevant OpenID Connect checks when authentication is involved.
Read RFC 7636: Proof Key for Code Exchange by OAuth Public Clients for the protocol details.
7. Validating OpenID Connect ID tokens
Using OpenID Connect does not automatically make a login implementation secure. The client must validate the ID token according to the OpenID Connect specification rather than simply decode its JSON payload and trust the values inside it.
A JSON Web Token can be decoded without verifying its signature. Decoding tells the application what claims the token contains, but it does not prove who issued the token or whether the claims are trustworthy.
Important validation checks
- Signature: Validate the token's signature using the appropriate trusted keys and an approved algorithm.
- Issuer: Confirm that the
issclaim matches the expected identity provider. - Audience: Confirm that the
audclaim includes the client application's registered identifier and apply the specification's additional rules where relevant. - Expiration: Reject expired tokens and validate the relevant time-based claims.
- Nonce: When a nonce was sent in the authentication request, verify that the returned ID token contains the expected nonce.
- Authorized party: When applicable, validate the
azpclaim and other audience-related requirements. - Authentication context: Validate additional claims if the application requires a particular authentication method or freshness.
These checks must be implemented using the rules appropriate to the OIDC flow and token type. A resource server validating an access token has a different responsibility from a client validating an ID token.
For server-side applications, use a maintained OIDC library that performs the required validation and supports the provider's discovery metadata and signing keys. Avoid writing a custom JWT verifier unless you have a specific, well-reviewed reason to do so.
8. Excessive scopes and permissions
OAuth scopes describe the access an application is requesting. A scope may allow an application to read profile information, access files, manage calendars or perform other actions supported by the authorization server.
Requesting more permissions than a feature needs increases the potential impact of a compromised application or stolen token. If a service only needs basic profile information, asking for access to a user's entire file collection would be difficult to justify.
Apply the principle of least privilege
- Request only the scopes required for the feature being implemented.
- Separate optional functionality from permissions needed for the initial experience.
- Explain why a permission is needed before requesting it when appropriate.
- Review provider-specific scopes carefully because similarly named scopes can grant very different levels of access.
- Remove permissions that the application no longer needs.
- Do not assume that a scope request guarantees that the server will grant the requested permissions.
Scope names and behavior vary between providers. Developers should use the provider's documentation and inspect the actual permissions granted, rather than assuming that all providers interpret a scope identically.
Least-privilege authorization reduces the potential damage from token compromise, but it does not replace secure token storage or server-side access controls.
9. Token storage and refresh-token security
OAuth tokens are credentials, and their storage should be designed around the capabilities and risks of the client platform.
Browser-based applications
Browser applications need to account for cross-site scripting, malicious dependencies and access by other scripts running in the same origin. A token available to JavaScript can potentially be stolen if an attacker gains script execution in the application.
For applications with a backend, a backend-for-frontend architecture can keep OAuth tokens on the server and give the browser a session cookie instead. That cookie must be configured appropriately, including suitable Secure, HttpOnly and SameSite attributes, alongside proper CSRF defenses where required.
For pure browser-based clients, token storage needs to be evaluated against the threat model. Browser storage is not automatically safe merely because it is persistent or tied to a particular origin.
Mobile applications
Native applications should use an established authorization library and the system browser or an appropriate secure browser-based authorization mechanism. Long-lived credentials should not be hard-coded into the application package.
On Android, sensitive local encryption keys can be protected using Android Keystore, where supported. However, storing an OAuth token in an encrypted file is only part of the design: the application must also protect the encryption key and avoid exposing the decrypted token to untrusted code.
Refresh tokens
Refresh tokens can be more sensitive than short-lived access tokens because they may be used to obtain new access tokens. Authorization servers should apply the protections appropriate to the client type and deployment.
For public clients, current OAuth security guidance recommends sender-constrained refresh tokens or refresh-token rotation to help detect or limit replay. With rotation, the authorization server issues a replacement refresh token and invalidates the previous one. Reuse of an invalidated token can indicate that a token has been copied, although the exact detection and response depend on the server's implementation.
Confidential clients should also protect refresh tokens, authenticate appropriately to the token endpoint, and apply the provider's expiration and revocation rules. Rotation is not a universal substitute for client authentication or secure storage.
Logout and revocation
Deleting a local session does not necessarily invalidate a token that has already been issued. Where supported, applications should use the provider's revocation mechanism when a token must be invalidated, and they should follow the provider's documented logout behavior.
Revocation, expiration and local logout are related but different operations. Developers should make clear which sessions and credentials are terminated by a logout action.
10. A practical OAuth security checklist
Before releasing an OAuth or OpenID Connect integration, developers and reviewers should verify the following areas.
- Choose the correct protocol. Use OpenID Connect for user authentication and OAuth for resource authorization.
- Use the authorization code flow with PKCE. Apply it to public clients and follow current security guidance for confidential clients.
- Validate authorization transactions. Bind callbacks to the correct session or transaction, validate state where used, and prevent callback reuse or injection.
- Validate redirect URIs correctly. Follow the applicable exact-matching requirements and reject unregistered destinations.
- Validate ID tokens. Verify signatures, issuer, audience, expiry and the additional claims required by the OIDC flow.
- Protect the token endpoint. Use appropriate client authentication for confidential clients and enforce PKCE verification.
- Minimize requested scopes. Request only the permissions necessary for the intended features.
- Protect credentials. Keep tokens out of URLs, logs, analytics events and error messages.
- Secure refresh tokens. Follow current rotation or sender-constraint guidance and provider-specific requirements.
- Handle errors safely. Reject invalid responses and avoid revealing credentials or internal security details in error messages.
- Review dependencies. Use maintained OAuth/OIDC libraries and keep them updated.
- Test negative cases. Verify that incorrect state values, unexpected redirect destinations, invalid tokens and failed PKCE checks are rejected.
Important: A successful login test does not prove that an OAuth integration is secure. Test both successful and unsuccessful authorization paths, including invalid callbacks and token-validation failures. Conduct testing only against applications and environments you own or have permission to assess.
Conclusion
OAuth 2.0 and OpenID Connect provide established mechanisms for delegated authorization and user authentication. Their security depends on correctly binding authorization requests to responses, validating redirect destinations, protecting authorization codes and tokens, and verifying identity claims before trusting them.
The most useful starting point for a new implementation is an established library configured according to current security guidance. Developers should avoid legacy flows, use PKCE for authorization code exchanges, apply the correct state and nonce checks, and request only the permissions their applications genuinely need.
Security reviewers should examine the complete flow rather than only the login screen. A working consent prompt is not evidence that callbacks, tokens, scopes and sessions are handled correctly. Reviewing these parts together helps identify weaknesses before they affect users.
Key Takeaways
- OAuth 2.0 handles authorization; OpenID Connect adds an identity and authentication layer.
- State helps bind an authorization response to the correct transaction and must be validated where used.
- Redirect URIs must be validated according to the applicable OAuth rules.
- Tokens should not be exposed in URLs, logs or analytics systems.
- Authorization code with PKCE is the recommended approach for modern interactive clients.
- ID tokens must be validated, not merely decoded.
- Least-privilege scopes limit the potential impact of compromised credentials.
- Refresh tokens require appropriate protection, and public clients should follow rotation or sender-constraint guidance.
- Maintained OAuth/OIDC libraries and negative-path testing reduce avoidable implementation mistakes.