Secure OAuth and OIDC as a Relying Party (OWASP ASVS 5.0 edition)
A practical, ASVS 5.0-aligned walkthrough of OAuth and OIDC relying party pitfalls. If you are stuck on the requirements, this guide will walk you through real examples with mitigations.
When your application acts as an OAuth client or OIDC relying party (delegating authentication or authorization to an external Identity Provider) you take on a specific set of security responsibilities. The identity provider handles credential verification, but you are responsible for everything that happens with the response.
Being a relying party can mean a few different things. You might be an OIDC relying party, simply delegating login to an external IdP, whether that’s a social provider like Google or a corporate one like Entra ID. Or you might be an OAuth client, obtaining tokens from an authorization server so you can integrate with some API on the user’s behalf.
This article works through the OWASP ASVS 5.0 requirements that apply specifically to the relying party role, covering the most common implementation mistakes and how to avoid them. It covers the following ASVS sections:
- V10.1 Generic OAuth and OIDC Security
- V10.2 OAuth Client
- V10.5 OIDC Client
- V6.8 Authentication with an Identity Provider
With these requirements as our checklist, we will walk through the OAuth and OIDC vulnerabilities they are designed to prevent.
Connect with the author on LinkedIn

Choosing the wrong OAuth architecture for your client type
Applies to: OAuth & OIDC (V10.1.1)
The most important OAuth decision in a browser-based application is not a configuration setting, but the architecture you choose before writing a line of code. That choice determines whether tokens can reach the browser at all. The IETF OAuth 2.0 for Browser-Based Apps Best Current Practice draft defines three architectures in descending order of protection:
- Backend-for-Frontend (BFF): The backend handles the full authorization code flow as a confidential client. No token of any kind is JavaScript-accessible. The browser holds only a session cookie.
- Token-Mediating Backend (TMB): The backend holds the refresh token and issues short-lived access tokens to the frontend on demand. The access token is JavaScript-accessible and therefore XSS-exposed. A conscious trade-off, not an equivalent alternative to BFF.
- Browser-based public client: The SPA holds both tokens directly. Appropriate only when there is genuinely no server-side component at all.
For traditional server-rendered web apps, the case is simple, as they are confidential clients by architecture. The problem of tokens in a JavaScript runtime does not exist for them. The BFF pattern exists precisely to give SPA architectures the same security posture that traditional web apps have always had.
A common mistake is an application that has a server-side backend but registers as a public client anyway, exposing tokens to the browser when nothing architectural requires it. This affects both traditional web apps misconfigured without a client secret, and SPAs that skip the BFF pattern despite having a backend available.
Mitigation:
- For applications with a server-side backend: Always register as a confidential client.
- For Browser-Based Apps: If a server-side component exists, implement BFF and register as a confidential client. If full BFF proxying is genuinely impossible, use TMB as a documented trade-off. Only register as a public client if there is no backend at all.
- Revisit your client type registration periodically. Applications that started as pure SPAs often grow a backend over time without the OAuth architecture being updated to match.
Sending tokens to components that don’t need them
Applies to: OAuth & OIDC (V10.1.1)
Even with the right architecture chosen, tokens can still end up in the wrong place at runtime. The three token types carry different levels of risk if they reach an unintended component:
- Refresh tokens are the highest-value target: long-lived and capable of silently obtaining new access tokens. Their exposure is effectively persistent compromise until revocation.
- Access tokens are shorter-lived but directly usable against resource servers. A stolen access token is immediately exploitable within its lifetime.
- ID tokens are identity assertions for the client’s own consumption, not credentials for API access. They should not be forwarded to components that have no need to know who the user is and only be used once, not as a session token.
The exposure vectors are rarely dramatic. Tokens leak through default-on logging that captures Authorization headers. They leak through active transmission: a frontend error logger configured to capture full request objects, an analytics SDK passed user context that happens to include a token, or a marketing platform receiving event data with authentication headers attached. They leak through a BFF that correctly handles the authorization code flow server-side, then passes the resulting access token to the SPA so the frontend can call APIs directly, negating the entire security property of the pattern. And they leak through service-to-service calls inside a backend that forward a raw incoming token to a downstream service rather than each service obtaining its own.
Mitigation:
- Never pass tokens to third-party scripts, analytics libraries, or advertising SDKs.
- When adopting BFF, verify tokens never leave the backend. Forwarding access tokens to the SPA defeats the purpose of the BFF pattern and effectively turns it into a misconfigured confidential client.
- Each backend service should obtain its own token with the minimum scopes it needs, rather than receiving a forwarded token from an upstream service. Token Exchange (RFC 8693) is the specified mechanism for this, allowing a service to exchange an incoming token for a scoped downstream one. However, few IdPs implement it today, so treat it as the target pattern where available rather than a baseline expectation.
- For ID tokens, consume them at the component that handles authentication and do not propagate them further. They are not a session credential.
- Configure HTTP clients, API gateways, and reverse proxies to redact Authorization headers from logs and traces. Token leakage through default-configured request logging is a routine real-world vector across all architectures.
Failing to bind the authorization flow to the session that started it
Applies to: OAuth (V10.1.2, V10.2.1) & OIDC (V10.1.2, V10.5.1)
An authorization response should only be accepted if it corresponds to a flow the current session actually initiated. Without this binding, an attacker can inject a valid authorization code or ID token obtained from a different session, causing the victim’s client to complete authentication under the attacker’s identity.
Three client-generated values provide this binding, each at a different stage and serving a distinct purpose.

See: OpenID Connect Nonce explained: Where it matters and where it doesn’t. Replay attack revisited
If you choose to implement PKCE, which is mandatory only for public clients, but recommended for confidential clients as well, state is no longer required for CSRF protection, but remains useful for carrying application context across the redirect (return URL, UI state, tenant identifier).
All three values share the same generation requirements: cryptographically random, sufficient entropy, single-use, and bound to the specific session and transaction that generated them.
Mitigation:
- Enforce PKCE for all clients. While mandatory only for public clients, there is no good reason to omit it for confidential clients. The code challenge method must be S256, never plain.
- Generate code verifier, nonce, and state (if used) using a cryptographically secure random source. Never derive them from predictable inputs. Each value should be different.
- Store the appropriate values in the user agent session at the start of the flow and verify them on return. A mismatch should be a hard rejection.
- For OIDC flows, always include and validate nonce in the ID token, as it is the required mitigation against ID token replay (V10.5.1). Its protective value varies by client type and how the ID token is consumed. For implementation guidance and a full discussion of when nonce carries meaningful weight, see OpenID Connect Nonce explained.
If you choose to implement PKCE, which is mandatory only for public clients, but recommended for confidential clients as well, state is no longer required for CSRF protection, but remains useful for carrying application context across the redirect (return URL, UI state, tenant identifier).
All three values share the same generation requirements: cryptographically random, sufficient entropy, single-use, and bound to the specific session and transaction that generated them.
Mitigation:
- Enforce PKCE for all clients. While mandatory only for public clients, there is no good reason to omit it for confidential clients. The code challenge method must be S256, never plain.
- Generate code verifier, nonce, and state (if used) using a cryptographically secure random source. Never derive them from predictable inputs. Each value should be different.
- Store the appropriate values in the user agent session at the start of the flow and verify them on return. A mismatch should be a hard rejection.
- For OIDC flows, always include and validate nonce in the ID token, as it is the required mitigation against ID token replay (V10.5.1). Its protective value varies by client type and how the ID token is consumed. For implementation guidance and a full discussion of when nonce carries meaningful weight, see OpenID Connect Nonce explained.
- Do not reuse any of these values across transactions.
Requesting scopes “just in case”
Applies to: OAuth (V10.2.3)
Over-provisioned scopes seem harmless during development. You request read:all write:all to avoid coming back later. In production, this means every token carries permissions the user never intended to delegate, turning every token theft into a maximum-damage event. For SaaS products, the cost is double: enterprise procurement teams routinely reject integrations that request permissions beyond what the stated functionality requires. A write scope on a read-only integration is an obvious red flag in a vendor security questionnaire.
Mitigation:
- Request the minimum set of scopes the current operation actually requires.
- For outbound integrations, define the minimum recommended scope set for each provider and present it as the default in your integration configuration. Do not leave scope selection entirely to the user. Your SaaS owns the integration definition and bears responsibility for what it requests.
- Make the scope list explicit and reviewable in your integration configuration, with plain-language explanations for each scope. Enterprise security reviewers will ask why your Jira integration needs write access, and the answer should be visible in your UI, not buried in documentation.
- Never hardcode a broad scope silently.
Accepting unsigned or tampered authentication assertions
Applies to: OAuth & OIDC (V6.8.2)
A JWT without a valid digital signature is just an unverified claim. Skipping signature validation, or accepting a token signed with an unexpected algorithm, means an attacker can authenticate as any user. Algorithm confusion attacks continue to surface in JWT implementations to this day, and key handling mistakes remain a reliable source of real CVEs.
Mitigation:
- Always validate the signature on every JWT. For asymmetric algorithms, verify it against the IdP’s published JWKS rather than a key supplied by the client or embedded in the token itself.
- When resolving a key by kid (Key ID), only look it up within your pre-fetched, trusted JWKS’. Never fetch or accept a key from a location referenced inside the token, as an attacker-controlled kid, jku, or x5u can otherwise point your library at an attacker’s key.
- Never derive the expected algorithm from the token header alone. The header is attacker-controlled, so the allowed algorithm must be configured server-side.
Trusting the issuer without checking who they are
Applies to: OAuth (V10.2.2) & OIDC (V10.2.2, V10.5.3)
Your client reaches out to what it believes is its configured IdP. But if you never verify the iss claim against the issuer value you established at configuration time, a malicious authorization server can impersonate a legitimate one. This risk is most dangerous when a single client interacts with multiple authorization servers, where token confusion can arise from ambiguous issuer binding. In that context, always treat the issuer as valid only for a specific audience.
The same principle applies during discovery. RFC 8414 §3.3 requires that the issuer field in the fetched metadata exactly matches the URL you used to retrieve it. RFC 8414 §2 specifies that a well-formed issuer identifier uses the https scheme with no query and no fragment. When we say exact strict matching, we mean exactly that.
Mitigation:
- String-compare the iss value in every token response against your pre-configured issuer value: no prefix matching, no trailing slash normalization, no case folding.
- When working with multiple authorization servers, always resolve the expected issuer for the specific audience in question.
- During discovery, reject metadata where the issuer field does not exactly match the URL used to fetch it.
- Treat any mismatch as a hard failure, not a warning.
Accepting tokens that were never meant for you
Applies to: OIDC (ASVS V10.5.4)
An ID token issued to one client should not be usable by another. Without audience validation, a token issued to a different application by the same IdP becomes a valid credential at yours.
Mitigation:
- If aud is a string, it must exactly equal your registered client_id. If aud is an array, your client_id must be present as a member. Unexpected additional values in aud should be treated as a risk signal.
Identifying users by mutable claims
Applies to: OIDC (V10.5.2, V6.8.1)
Email and preferred_username are convenient and human-readable, but both can be reassigned when a user changes their address or an admin recycles an account. Build your internal user identity around either of those, and youare heading towards an account takeover.
The nOAuth vulnerability, disclosed by Descope in 2023, showed how bad this can get. In Entra ID, the email claim is both mutable and unverified, which means an attacker with admin access to any Entra ID tenant can change their account’s email attribute to a victim’s address, then use “Log in with Microsoft” on any application that keys users by email. No credential theft required. Several applications with millions of users turned out to be vulnerable, including cases where authentication providers were merging accounts on matching email without any validation.
The problem gets worse when your application supports multiple IdPs. Without namespacing identities by their source IdP, a user registered via one tenant’s IdP could be spoofed by an attacker registering the same sub value through a different, attacker-controlled IdP you also trust.
Mitigation:
- Never use email, preferred_username, or any other mutable claim as a primary user key.
- Use the compound iss + sub pair as your immutable user identifier (issuer acts as a namespace, subject as the identity within it).
- When supporting multiple IdPs, always scope identity lookups by IdP. Never look up a user by sub alone across all IdPs.
- If you merge accounts across authentication methods (e.g., linking an existing email account to Log in with Microsoft), validate ownership of the email address independently. Don’t rely on the IdP’s email claim as proof of ownership.
- Surface the effective identifier explicitly in your integration UI so developers understand what they’re keying against.
Skipping back-channel logout or validating it too loosely
Applies to: OIDC (V10.5.5)
Most SSO integrations handle login carefully and logout as an afterthought. Both front-channel and back-channel logout are rarely implemented by relying parties, but back-channel logout is the more reliable solution and the one we will focus on.
When a user logs out at the IdP (or an admin revokes a session, or a security event triggers forced logout), the IdP sends a logout request to your application along with a signed JWT, which carries the user’s session information. The application is expected to immediately invalidate that session server-side.
We will discuss back-channel logout in more detail, including IdP support and the SaaS adoption question, in my next article.
Mitigation:
- Implement back-channel logout. Its absence is the most common gap in this area.
- Validate the logout token carefully to make sure that it is not a replayed ID Token (cross-JWT confusion). Make sure that it is correctly typed (logout+jwt), contains a proper events claim, and doesn’t contain a nonce. Reject any logout token that contains a nonce claim. Of course, standard JWT validation also applies.
Ignoring authentication context
Applies to: OIDC (V6.8.4)
Even when authentication is delegated to an external Identity Provider, the Relying Party can still require a specific authentication strength or recentness. This is often not enforced by Relying Parties, even though OIDC provides the signals needed to do so. In practice, this control is most relevant in organization-controlled integrations, where both the service provider and the IdP are managed by the same company.
The required authentication context can usually be requested in the authorization request and verified from ID Token claims such as acr (Authentication Context Class Reference), amr (Authentication Methods Reference), and auth_time (time when the End-User authentication occurred).
Mitigation:
- Consider requesting the required authentication context (acr_values) and verify the returned acr, amr, and auth_time claims where available.
- Treat missing context claims as the lowest assurance level, not as implicit approval.