OAuth Security Testing: Common Misconfigurations and Attack Paths

OAuth Security Testing: Common Misconfigurations and Attack Paths

OAuth 2.0 is one of those specifications that’s simple to describe at a high level and remarkably easy to implement insecurely in practice. I’ve tested “Login with Google” and “Login with Facebook” style flows across dozens of applications, and the same handful of implementation mistakes — not flaws in the OAuth spec itself, but in how developers wire it up — keep appearing. In this guide, I’ll walk through OAuth security testing methodology: how the flows actually work, the common misconfigurations, and exactly how I test for each one.

What OAuth Is and Why Its Security Matters

OAuth 2.0 is an authorization framework that lets a user grant a third-party application limited access to their resources on another service, without sharing their password directly. When you click “Sign in with Google” on a website, you’re going through an OAuth flow: the site (the “client”) redirects you to Google (the “authorization server”), you authenticate and approve the requested permissions, and Google redirects you back to the site with a code that gets exchanged for an access token.

It matters because OAuth sits directly at the authentication boundary for a massive share of modern web applications. A flaw in how a client implements OAuth can lead to full account takeover — an attacker logging in as another user without ever knowing their password — which makes OAuth misconfigurations some of the highest-impact bugs I encounter in web application assessments.

It’s worth being precise about terminology here: OAuth 2.0 is technically an authorization framework, not an authentication protocol. OpenID Connect (OIDC) is built on top of OAuth 2.0 specifically to add a standardized authentication layer (the ID token). A lot of real-world “Login with X” implementations blur this line, and that confusion itself is a common source of vulnerabilities.

Lab Setup for Legal Practice

I test OAuth flows using deliberately vulnerable applications and self-hosted authorization servers:

  1. PortSwigger’s Web Security Academy OAuth labs — a well-structured, guided set of scenarios covering most real-world misconfigurations.
  2. A self-hosted OAuth provider (like Keycloak or a simple custom Node.js authorization server) paired with a deliberately misconfigured client app for practice.
  3. Burp Suite, essential for intercepting and manipulating the redirect-based flow at every step.
  4. A second browser profile or incognito session, useful for simulating victim and attacker sessions separately during testing.
docker run -p 8080:8080 quay.io/keycloak/keycloak start-dev

What this does: launches a local Keycloak instance, a full-featured open-source identity and access management server, letting you configure realistic OAuth/OIDC clients and deliberately introduce misconfigurations to practice against.

Understanding the Authorization Code Flow

Before testing, it’s worth being clear on the standard flow, since most vulnerabilities are deviations from this expected sequence:

  1. The client redirects the user to the authorization server’s /authorize endpoint, including a client_id, redirect_uri, scope, state, and response_type=code.
  2. The user authenticates and approves the requested scopes.
  3. The authorization server redirects back to the client’s redirect_uri with an authorization code and the original state value.
  4. The client’s backend exchanges that code (plus its client_secret) for an access token via a direct server-to-server request to the /token endpoint.
  5. The client uses the access token to fetch user info and establish a session.

Methodology: Step-by-Step OAuth Testing

Step 1: Map the Full Flow

I intercept the entire flow in Burp from the initial redirect through to the final session establishment, noting every parameter passed at each step: client_id, redirect_uri, scope, state, response_type, and how the returned code or token is ultimately used to authenticate the user.

Step 2: Test redirect_uri Validation

This is the single most common and highest-impact OAuth misconfiguration I find. If the authorization server doesn’t strictly validate the redirect_uri against a registered allowlist, an attacker can redirect the authorization code (or token, in implicit flow) to a server they control.

https://auth-provider.lab/authorize?client_id=abc123&redirect_uri=https://legit-app.lab.evil-attacker.lab&response_type=code&scope=profile

Purpose: this tests whether the authorization server performs strict, exact-match validation of redirect_uri, or whether it accepts subdomain tricks, path traversal, or open redirect chaining (redirect_uri=https://legit-app.lab/redirect?next=https://evil.lab). A successful bypass means the authorization code gets delivered straight to an attacker-controlled endpoint.

I test several validation bypass patterns:

redirect_uri=https://legit-app.lab.attacker.lab/callback
redirect_uri=https://legit-app.lab@attacker.lab/callback
redirect_uri=https://legit-app.lab/callback%2F..%2Fattacker
redirect_uri=https://legit-app.lab/callback?redirect=https://attacker.lab

Purpose: these test common weak validation logic — substring matching instead of exact matching, URL parser confusion with the @ symbol, path normalization issues, and open-redirect chaining through a legitimate but overly permissive callback parameter.

Step 3: Test the state Parameter for CSRF Protection

The state parameter exists specifically to prevent CSRF attacks against the OAuth flow — without it, an attacker can trick a victim into completing an OAuth flow using the attacker’s own authorization code, effectively linking the victim’s session to the attacker’s third-party account.

GE T /oauth/callback?code=ATTACKER_CODE HTTP/1.1

I test whether the application:

  1. Requires the state parameter at all.
  2. Validates that the returned state matches the one originally issued to that specific session.
  3. Uses a sufficiently random, unpredictable state value rather than something guessable.

Purpose: if I can complete the OAuth callback flow using a code obtained through my own account, delivered via a crafted link to a victim without a valid matching state, I can potentially link the victim’s application account to my attacker-controlled third-party identity — a serious account-linking CSRF vulnerability.

Step 4: Test Authorization Code Reuse and Expiry

Authorization codes are meant to be single-use and short-lived. I test whether a code can be exchanged for a token more than once, or long after issuance:

curl -X POST https://auth-provider.lab/token \
  -d "grant_type=authorization_code&code=<captured_code>&client_id=abc123&client_secret=<secret>"

Then I immediately replay the exact same request a second time.

Purpose: if the second request also succeeds and returns a valid token, the authorization server isn’t properly invalidating codes after use, which extends the window during which an intercepted code remains dangerous — for example, in scenarios where a code leaks through browser history, referrer headers, or logging.

Step 5: Test Scope Manipulation

I test whether requesting a broader or different scope than the client was originally configured for succeeds:

https://auth-provider.lab/authorize?client_id=abc123&redirect_uri=https://legit-app.lab/callback&response_type=code&scope=profile+admin+read:all

Purpose: this checks whether the authorization server enforces which scopes a given client_id is actually permitted to request, or whether it blindly grants whatever scope the request specifies — potentially letting a client application (or attacker manipulating the request) obtain permissions it was never meant to have.

Step 6: Test the Implicit Flow (If Present) for Token Leakage

Some legacy or misconfigured implementations still use the implicit flow, where the access token is returned directly in the URL fragment rather than through a backend code exchange. I check for this and, if present, test whether the token leaks via:

  • Referrer headers, if the page containing the token in its URL loads any third-party resources.
  • Browser history, since fragment-embedded tokens persist there.
  • Open redirects, chaining an insecure redirect_uri into full token theft rather than just code theft.

Purpose: implicit flow inherently exposes tokens to more leakage vectors than the authorization code flow, which is why OAuth 2.1 guidance recommends deprecating implicit flow entirely in favor of authorization code flow with PKCE.

Step 7: Test PKCE Implementation (for Public Clients)

Proof Key for Code Exchange (PKCE) is designed to protect public clients (mobile apps, SPAs) that can’t securely store a client_secret. I test whether PKCE is actually enforced when it should be:

https://auth-provider.lab/authorize?client_id=abc123&redirect_uri=https://legit-app.lab/callback&response_type=code&code_challenge=&code_challenge_method=S256

Purpose: this tests whether the authorization server rejects a request with a missing or empty code_challenge for a public client, or whether it silently falls back to a non-PKCE flow — which would reintroduce authorization code interception risk that PKCE was specifically designed to close.

Step 8: Test Account Linking Logic

Many applications let users link multiple OAuth providers (Google, GitHub, Facebook) to a single account. I test whether the linking logic properly verifies email ownership/verification status from each provider before merging accounts:

Purpose: if the application trusts an unverified email claim from a third-party provider and automatically links or merges it with an existing account matching that email, an attacker could register an OAuth identity with a spoofed or unverified email matching a victim’s account, potentially achieving account takeover through the linking flow itself.

Step 9: Test for OAuth Consent Phishing Susceptibility

Beyond technical flow vulnerabilities, I evaluate how the consent screen itself is presented, since a well-implemented technical flow can still be abused socially if the consent UI doesn’t clearly communicate what’s being requested:

Purpose: I review whether the requested scopes are displayed in plain, specific language (e.g., “read your email contacts” rather than a vague “access your account”), and whether the client application’s identity is clearly and unspoofably presented on the consent screen. Poorly designed consent screens make it easier for attackers to register malicious OAuth applications that trick users into granting broad access under a misleading or generic-sounding app name — a real attack pattern seen in several documented supply-chain-style OAuth phishing campaigns.

Step 10: Test Device Authorization Grant Flow (If Present)

Applications targeting devices with limited input capabilities (smart TVs, CLI tools) sometimes implement the device authorization grant, where a user approves access on a secondary device using a short code. I test this flow specifically for:

https://auth-provider.lab/device?user_code=ABCD-1234

Purpose: this checks whether user codes are sufficiently long and rate-limited against brute-forcing, since a short, unrate-limited code space could let an attacker guess a valid pending code and approve access to their own device instead of the legitimate one, or hijack a session mid-flow if the code space is small enough to enumerate within the flow’s expiry window.

Common Mistakes and Troubleshooting Tips

  • Only testing the exact registered redirect_uri. The real value is in testing edge cases and bypass patterns around that URI, not just confirming the happy path works.
  • Skipping state parameter testing because it “looks present.” Presence alone isn’t enough — test whether it’s actually validated server-side and generated with sufficient entropy.
  • Not distinguishing between the authorization server’s flaws and the client application’s flaws. A perfectly secure identity provider (like Google or GitHub) can still be integrated insecurely by the client application; you’re often testing the client’s implementation, not the provider itself.
  • Forgetting to test scope enforcement. Many teams assume scopes are purely cosmetic and don’t validate that a client’s requested scope aligns with what it’s actually authorized for.
  • Overlooking token storage after the flow completes. Even a perfectly executed OAuth flow can be undermined if the resulting session token is stored insecurely on the client side afterward.
  • Testing against production identity providers without authorization. Always use sandboxed or self-hosted OAuth providers, or applications with explicit test accounts and scope authorized for this kind of testing.

Security Risks and Defensive Recommendations

For teams implementing OAuth-based login, the recommendations I consistently give:

  • Enforce exact-match redirect_uri validation against a strict allowlist — no wildcard subdomains, no substring matching, no path-based leniency.
  • Always use and validate the state parameter, generated with sufficient entropy and tied to the user’s session, to prevent CSRF against the OAuth callback.
  • Invalidate authorization codes immediately after first use and enforce short expiry windows.
  • Enforce PKCE for all public clients, and consider requiring it universally as recommended by current OAuth 2.1 guidance.
  • Validate that requested scopes match what a given client is actually authorized for, rather than trusting the request blindly.
  • Prefer the authorization code flow over implicit flow entirely; deprecate implicit flow if it’s still in use.
  • Verify email ownership before account linking, and never auto-merge accounts based solely on an unverified claim from a third-party provider.

Testing Token Exchange and Refresh Token Rotation

For applications implementing refresh token rotation (where each use of a refresh token issues a new one and invalidates the old), I specifically test whether reuse detection is actually implemented:

Purpose: I capture a refresh token, use it once to obtain a new access/refresh token pair, then attempt to reuse the original (now-superseded) refresh token a second time. Properly implemented rotation should detect this reuse as a signal of potential token theft and revoke the entire token family as a precaution. If the old token still works, it indicates the server tracks validity per-token rather than per-family, meaning a stolen refresh token can be used indefinitely in parallel with the legitimate user’s session without triggering any automatic revocation response.

Frequently Asked Questions

Is OAuth 2.0 itself insecure? No, the specification is sound when implemented correctly. The vast majority of real-world vulnerabilities come from implementation mistakes by individual client applications, not flaws in the OAuth standard itself.

What’s the difference between OAuth and OpenID Connect? OAuth 2.0 is an authorization framework for granting access to resources; OpenID Connect is built on top of OAuth 2.0 specifically to standardize authentication, adding the ID token and a consistent way to verify user identity.

Why is redirect_uri validation so critical? Because it’s the delivery mechanism for the authorization code (or token, in implicit flow) — if an attacker can redirect that delivery to a server they control, they can potentially complete the OAuth flow as the victim or steal their session.

Does PKCE replace the need for a client_secret? For public clients (SPAs, mobile apps) that can’t securely store a secret, yes — PKCE provides equivalent protection against authorization code interception without requiring a stored secret. Confidential clients (backend servers) should still use a client_secret in addition to PKCE where supported.

Can I test OAuth vulnerabilities against real providers like Google or Facebook? You should never test the identity provider’s own infrastructure without explicit authorization; instead, test how a specific client application you’re authorized to assess integrates with that provider, using your own test accounts.

What’s the most common OAuth vulnerability found in bug bounty programs? redirect_uri validation bypasses and missing or unvalidated state parameters are consistently among the most frequently reported and rewarded OAuth-related findings.

Should implicit flow still be used in new applications? No, current best practice (reflected in OAuth 2.1 guidance) recommends against implicit flow entirely in favor of the authorization code flow with PKCE, due to the inherent token leakage risks of returning tokens directly in URL fragments.

Conclusion

OAuth security testing is less about breaking cryptography and more about carefully tracing a well-defined flow and checking whether each step actually enforces the protections it’s supposed to — strict redirect validation, a properly checked state parameter, single-use authorization codes, and correctly scoped tokens. Because the spec itself is sound, nearly every real-world vulnerability traces back to an implementation shortcut somewhere in the client application’s integration. Map the flow carefully, test each parameter’s validation logic deliberately, and you’ll consistently find the account takeover and CSRF-adjacent bugs that make OAuth misconfigurations some of the highest-impact findings in modern web application assessments.

For related authentication testing methodology, see my guides on JWT security testing and broken access control testing.

References

  • OAuth 2.0 Security Best Current Practice (RFC 9700)
  • PortSwigger Web Security Academy, OAuth authentication labs
  • OWASP OAuth 2.0 Security Cheat Sheet
Total
0
Shares

Leave a Reply

Previous Post
How to Become a Penetration Tester in 2026 Complete Career Roadmap

How to Become a Penetration Tester in 2026: Complete Career Roadmap

Next Post
GraphQL API Penetration Testing: Complete Security Guide

GraphQL API Penetration Testing: Complete Security Guide

Related Posts