Back to Learning Center
criticalA07:2021CWE-287CWE-290CWE-347

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:

xml
<!-- 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:

xml
<!-- 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:

javascript
// 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 expires

OAuth 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:

javascript
// 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 flow

Open Redirect in OAuth

javascript
// 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.com

Authorization Code Interception

javascript
// 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 client

Prevention Strategies

Secure SAML Implementation

javascript
// 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

javascript
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

javascript
// 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

javascript
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 all