OAuth and SAML Security
OAuth 2.0 and SAML are the foundation of modern authentication systems, enabling single sign-on (SSO) across applications. However, their complexity creates numerous attack vectors. Implementation flaws in these protocols have led to critical authentication bypasses affecting major platforms including Fortinet, GitLab, and enterprise SSO providers.
SAML Authentication Bypass
SAML has been plagued by high-impact vulnerabilities due to the complexity of XML signature processing and token handling. Key vulnerability classes include XML signature wrapping, incorrect XML canonicalization, replay attacks, and implementation-specific flaws.
Fortinet FortiGate Attack (December 2025)
Critical vulnerabilities allowed unauthenticated bypass of SSO login authentication via crafted SAML messages when FortiCloud SSO was enabled. Attackers used this to perform malicious SSO logins against admin accounts and export device configurations.
Ruby-SAML Parser Differential (CVE-2025-25291/25292)
Critical authentication bypass vulnerabilities in ruby-saml exploited differences between REXML and Nokogiri XML parsers. This affected GitLab and other platforms using the library, allowing attackers to forge valid SAML assertions.
XML Signature Wrapping Attacks
XML signature wrapping exploits how SAML processors validate signatures versus how they extract identity claims. An attacker can inject additional XML elements that change the authenticated identity while preserving signature validity:
<!-- Original signed SAML assertion -->
<saml:Assertion ID="_abc123">
<saml:Subject>
<saml:NameID>user@company.com</saml:NameID>
</saml:Subject>
<ds:Signature>
<!-- Valid signature over original assertion -->
</ds:Signature>
</saml:Assertion>
<!-- Wrapped attack - injected assertion processed first -->
<saml:Response>
<saml:Assertion ID="_evil">
<saml:Subject>
<saml:NameID>admin@company.com</saml:NameID> <!-- Attacker identity -->
</saml:Subject>
</saml:Assertion>
<saml:Assertion ID="_abc123"> <!-- Original signed assertion -->
<saml:Subject>
<saml:NameID>user@company.com</saml:NameID>
</saml:Subject>
<ds:Signature><!-- Still valid --></ds:Signature>
</saml:Assertion>
</saml:Response>SAML XML Injection
SAML responses can be modified through XML injection where attackers inject additional XML to change message structure, add roles, modify receivers, or inject entirely new usernames. Response signatures do not protect against this vulnerability:
<!-- Vulnerable: User-controlled data in SAML assertion -->
<saml:Attribute Name="email">
<saml:AttributeValue>
user@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="role">
<saml:AttributeValue>admin<!-- Injected! -->
</saml:AttributeValue>
</saml:Attribute>OAuth 2.0 Vulnerabilities
Token Replay Attacks
Both OAuth and SAML tokens can be intercepted and reused for unauthorized access. Without proper replay detection, attackers can capture tokens and use them repeatedly within their validity window:
// Captured OAuth token being replayed
const stolenToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
// Attacker reuses token multiple times
fetch('https://api.target.com/admin/users', {
headers: {
'Authorization': `Bearer ${stolenToken}`
}
});
// Without replay detection or token binding,
// this works until token expiresOAuth Mix-Up Attack
In OAuth mix-up attacks, a malicious authorization server tricks the client into sending tokens to the wrong endpoint. Microsoft and other providers have been affected by this token injection vulnerability:
// Vulnerable OAuth client with multiple IdPs
// Attacker controls malicious-idp.com
// Step 1: User initiates login with malicious IdP
window.location = 'https://malicious-idp.com/auth?' +
'client_id=victim-app&redirect_uri=https://victim.com/callback';
// Step 2: Malicious IdP redirects to legitimate IdP
// but manipulates the state parameter
// Step 3: Legitimate IdP issues token
// Step 4: Token sent to malicious IdP's endpoint
// because client confused about which IdP initiated flowOpen Redirect in OAuth
// VULNERABLE: Loose redirect_uri validation
const allowedRedirects = ['https://app.company.com'];
function validateRedirect(uri) {
// Only checks if URI starts with allowed domain
return allowedRedirects.some(allowed => uri.startsWith(allowed));
}
// Attack: Bypass with path traversal
// https://app.company.com.attacker.com/steal
// https://app.company.com/../../../attacker.com
// https://app.company.com@attacker.comAuthorization Code Interception
// Without PKCE, authorization codes can be intercepted
// on mobile apps or via open redirects
// VULNERABLE: No PKCE
const authUrl = `https://auth.provider.com/authorize?
client_id=${clientId}&
redirect_uri=${redirectUri}&
response_type=code&
scope=openid profile`;
// Attacker intercepts the authorization code
// and exchanges it for tokens before legitimate clientPrevention Strategies
Secure SAML Implementation
// Secure SAML assertion validation
function validateSAMLAssertion(assertion, expectedIssuer) {
// 1. Validate XML signature BEFORE processing any data
if (!validateXMLSignature(assertion)) {
throw new Error('Invalid signature');
}
// 2. Verify the signature covers the entire assertion
const signedElements = getSignedElementIds(assertion);
if (!signedElements.includes(assertion.id)) {
throw new Error('Assertion not signed');
}
// 3. Validate issuer matches expected IdP
if (assertion.issuer !== expectedIssuer) {
throw new Error('Invalid issuer');
}
// 4. Check assertion timestamps
const now = Date.now();
if (now < assertion.notBefore || now > assertion.notOnOrAfter) {
throw new Error('Assertion expired or not yet valid');
}
// 5. Verify audience restriction
if (!assertion.audience.includes(ourEntityId)) {
throw new Error('Invalid audience');
}
// 6. Check for replay (one-time use)
if (await isAssertionReplayed(assertion.id)) {
throw new Error('Assertion already used');
}
await markAssertionUsed(assertion.id);
return assertion.subject;
}Secure OAuth Implementation with PKCE
import crypto from 'crypto';
// Generate PKCE challenge
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// Authorization request with PKCE
const { verifier, challenge } = generatePKCE();
// Store verifier securely for token exchange
sessionStorage.setItem('pkce_verifier', verifier);
const authUrl = new URL('https://auth.provider.com/authorize');
authUrl.searchParams.set('client_id', clientId);
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('state', crypto.randomUUID()); // CSRF protection
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// Token exchange includes verifier
async function exchangeCode(code) {
const verifier = sessionStorage.getItem('pkce_verifier');
const response = await fetch('https://auth.provider.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: verifier // PKCE proof
})
});
return response.json();
}Strict Redirect URI Validation
// SECURE: Exact match redirect URI validation
const ALLOWED_REDIRECTS = new Set([
'https://app.company.com/callback',
'https://app.company.com/oauth/callback'
]);
function validateRedirectUri(uri) {
// Normalize and parse the URI
let parsed;
try {
parsed = new URL(uri);
} catch {
return false;
}
// Reconstruct without fragments or query params for comparison
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
// Exact match only - no partial matching
return ALLOWED_REDIRECTS.has(normalized);
}Token Validation Best Practices
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: 'https://auth.provider.com/.well-known/jwks.json',
cache: true,
rateLimit: true
});
async function validateToken(token) {
// Decode header to get key ID
const decoded = jwt.decode(token, { complete: true });
// Fetch signing key from JWKS
const key = await client.getSigningKey(decoded.header.kid);
// Verify with strict options
const payload = jwt.verify(token, key.getPublicKey(), {
algorithms: ['RS256'], // Only allow expected algorithm
issuer: 'https://auth.provider.com',
audience: 'our-client-id',
clockTolerance: 30 // 30 second clock skew tolerance
});
// Additional checks
if (payload.azp && payload.azp !== 'our-client-id') {
throw new Error('Invalid authorized party');
}
return payload;
}Security Checklist
• Sign SAML assertions at both response and assertion level
• Implement replay detection for SAML assertions (one-time use)
• Validate SAML issuer matches expected Identity Provider
• Use exact-match redirect URI validation for OAuth
• Always implement PKCE for OAuth authorization code flow
• Use state parameter to prevent CSRF attacks
• Verify token audience and issuer claims
• Set short token lifetimes and implement refresh token rotation
• Keep SAML libraries updated - parser vulnerabilities are common
• Perform proper input validation on all SAML/OAuth data
Practice Challenges
View allOAuth Open Redirect
OAuth callback with open redirect. Steal the code.
OAuth State Missing
OAuth without state parameter. CSRF login attack.
OAuth Token Leak
Access token in URL fragment. Leaked via Referer.
OAuth Scope Escalation
Request more scopes than allowed. Server doesn't validate.
SAML Signature Bypass
SAML assertion signature not properly validated.