Auth Bypass with Openid Connect
How Auth Bypass Manifests in Openid Connect
OpenID Connect (OIDC) auth bypass vulnerabilities exploit the protocol's complexity and the subtle differences between its implementation options. Unlike OAuth 2.0's simpler authorization flows, OIDC adds identity tokens and discovery mechanisms that create additional attack surfaces.
The most common OIDC auth bypass occurs through nonce validation bypass. When implementing the implicit or hybrid flow, applications must validate the nonce parameter in the ID token against the original request. A missing or improperly validated nonce allows attackers to replay authentication responses:
# Vulnerable OIDC implementation
import jwt
def verify_id_token(id_token, client_secret):
# BUG: No nonce validation
decoded = jwt.decode(id_token, client_secret, algorithms=['HS256'])
return decoded
# Attacker intercepts ID token and replays it
stolen_id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
user_data = verify_id_token(stolen_id_token, client_secret)
# Attacker now authenticated as victim
Another critical OIDC bypass vector is client authentication bypass. OIDC clients can authenticate using client secrets, private keys, or JWT assertions. If an application accepts unsigned JWT assertions or fails to validate the client_id in the assertion, attackers can forge authentication:
# Vulnerable client authentication
from authlib.jose import jwt
def validate_client_assertion(assertion, expected_client_id):
# BUG: Doesn't verify client_id matches
claims = jwt.decode(assertion, verify=False)
return claims
# Attacker creates assertion with different client_id
fake_assertion = jwt.encode({
'iss': 'evil-client',
'sub': 'evil-client',
'aud': 'https://auth.example.com'
}, 'any-secret', algorithm='HS256')
claims = validate_client_assertion(fake_assertion, 'legitimate-client')
# BUG: Accepts forged client assertion
Discovery endpoint manipulation is another OIDC-specific bypass. The .well-known/openid-configuration endpoint can be poisoned or redirected, causing clients to use malicious authorization endpoints:
# Vulnerable discovery usage
import requests
def get_oidc_config(issuer):
# BUG: No HTTPS verification or endpoint validation
response = requests.get(f"{issuer}/.well-known/openid-configuration")
return response.json()
# Attacker controls DNS or uses evil issuer
malicious_config = get_oidc_config('https://evil-auth.com')
# Returns attacker-controlled endpoints
Session fixation through state parameter bypass is particularly dangerous in OIDC. The state parameter prevents CSRF attacks, but if implementations don't validate it properly or allow state to be optional, attackers can fixate sessions:
# Vulnerable state handling
from flask import Flask, session, request
app = Flask(__name__)
@app.route('/login/callback')
def oidc_callback():
# BUG: State validation missing
id_token = request.args.get('id_token')
verify_id_token(id_token, client_secret)
session['user'] = 'authenticated'
return 'Login successful'
# Attacker crafts URL with known state and tricks victim
# https://yourapp.com/login/callback?id_token=STOLEN_TOKENOpenID Connect-Specific Detection
Detecting OIDC auth bypass requires understanding the protocol's specific implementation patterns and testing them systematically. middleBrick's OIDC scanner performs 12 parallel security checks that specifically target OpenID Connect vulnerabilities.
The scanner first examines your OIDC discovery endpoint for configuration issues:
{
"openid-configuration": {
"issuer": "https://your-auth.com",
"authorization_endpoint": "https://your-auth.com/auth",
"token_endpoint": "https://your-auth.com/token",
"userinfo_endpoint": "https://your-auth.com/userinfo",
"jwks_uri": "https://your-auth.com/jwks.json",
"id_token_signing_alg_values_supported": ["RS256", "HS256"],
"claims_supported": ["sub", "aud", "exp", "nonce"],
"service_documentation": "https://your-auth.com/docs"
}
}
# middleBrick flags:
- Missing nonce in claims_supported
- Weak signing algorithms (HS256 without client secret validation)
- Missing userinfo_endpoint (critical for token validation)
- No HTTPS enforcement on endpoints
middleBrick actively tests nonce validation by:
- Capturing ID tokens during legitimate login flows
- Modifying or removing the nonce claim
- Attempting to replay the modified token
- Verifying if authentication succeeds without proper nonce validation
For client authentication bypass detection, the scanner attempts:
# Testing unsigned JWT assertions
curl -X POST https://auth.example.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_assertion=eyJhbGciOiJub25l...&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
# Testing modified client_id in assertions
curl -X POST https://auth.example.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_assertion=eyJhbGciOiJIUzI1Ni...&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
The scanner also validates your state parameter implementation by:
- Checking if state is required and validated
- Testing for state fixation vulnerabilities
- Verifying CSRF protection mechanisms
middleBrick's OpenAPI analysis cross-references your OIDC implementation with your API spec, flagging mismatches between documented authentication requirements and actual implementation.
OpenID Connect-Specific Remediation
Fixing OIDC auth bypass requires implementing the protocol correctly with specific attention to OpenID Connect's unique requirements. Here's how to remediate the vulnerabilities we identified:
Proper nonce validation is non-negotiable in OIDC:
from authlib.integrations.flask_client import OpenIDConnect
from flask import Flask, session, redirect, url_for, request
app = Flask(__name__)
app.config.update({
'OIDC_CLIENT_SECRETS': 'client_secrets.json',
'OIDC_SCOPES': ['openid', 'email', 'profile'],
'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post'
})
oidc = OpenIDConnect(app)
@app.route('/login')
def login():
# Generate secure nonce
nonce = secrets.token_urlsafe(32)
session['expected_nonce'] = nonce
return oidc.authorize_redirect(redirect_uri=url_for('callback', _external=True), nonce=nonce)
@app.route('/callback')
@oidc.require_login
def callback():
# OIDC library handles nonce validation automatically
user = session['user']
return f"Welcome {user}!"
# client_secrets.json should include:
{
"web": {
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"auth_uri": "https://your-auth.com/auth",
"token_uri": "https://your-auth.com/token",
"issuer": "https://your-auth.com",
"redirect_uris": ["https://yourapp.com/callback"]
}
}
Secure client authentication requires proper validation of JWT assertions:
from authlib.jose import jwt
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
# Verify client assertion using public key
def validate_client_assertion(assertion, expected_client_id, public_key):
try:
# Verify signature first
claims = jwt.decode(assertion, public_key, algorithms=['RS256'])
# Validate client_id matches
if claims.get('client_id') != expected_client_id:
raise ValueError('Client ID mismatch')
# Validate audience and issuer
if claims.get('aud') != 'https://auth.example.com':
raise ValueError('Invalid audience')
return claims
except Exception as e:
raise ValueError(f'Invalid client assertion: {e}')
# Store public keys securely and rotate regularly
PUBLIC_KEYS = {
'client-id': "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkq..."
}
Secure discovery endpoint usage requires validation and HTTPS enforcement:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_oidc_config(issuer):
if not issuer.startswith('https://'):
raise ValueError('Issuer must use HTTPS')
# Configure secure HTTP client
session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
session.mount('https://', HTTPAdapter(max_retries=retries))
try:
response = session.get(f"{issuer}/.well-known/openid-configuration",
timeout=10, verify=True)
response.raise_for_status()
config = response.json()
# Validate required endpoints exist
required = ['authorization_endpoint', 'token_endpoint', 'jwks_uri']
for endpoint in required:
if endpoint not in config:
raise ValueError(f'Missing required OIDC endpoint: {endpoint}')
return config
except requests.RequestException as e:
raise ValueError(f'OIDC discovery failed: {e}')
State parameter implementation must be mandatory and validated:
import secrets
from flask import Flask, session, redirect, url_for, request
app = Flask(__name__)
@app.route('/login')
def login():
state = secrets.token_urlsafe(32)
session['oidc_state'] = state
# Include state in authorization request
auth_url = f"https://auth.example.com/auth?response_type=code&client_id={CLIENT_ID}"
auth_url += f"&redirect_uri={REDIRECT_URI}&scope=openid%20email&state={state}"
return redirect(auth_url)
@app.route('/callback')
def callback():
# Validate state matches
received_state = request.args.get('state')
if received_state != session.get('oidc_state'):
return 'CSRF attempt detected', 403
# Continue with code exchange
code = request.args.get('code')
# ... validate code and exchange for tokens
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |