#!/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 [--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()