~/learn / authorization / single-sign-on

Single Sign-On with OIDC and SAML

How OpenID Connect and SAML let one login work across many apps, exactly what each token proves, why XML signature wrapping made SAML so fragile, and where SSO integrations go wrong.

intermediateupdated

Single sign-on means logging in once, at one identity provider, and being trusted by many applications. The work account that opens email, chat, and HR with a single login is SSO. Two protocols dominate: OpenID Connect for modern apps and SAML for older enterprise ones. Both answer the same question, “has this identity provider vouched for this user,” and both live or die on how carefully the receiving app validates the answer.

OpenID Connect: identity on top of OAuth

OAuth answers “may this app access my data?” and deliberately says nothing about who the user is. OpenID Connect (OIDC) adds exactly that, reusing the OAuth authorization code flow with two additions:

  • The client requests the openid scope.
  • The authorization server, now called the identity provider, returns an ID token alongside the access token.

The ID token is a signed JWT:

{
  "iss": "https://login.example.com",
  "sub": "248289761001",
  "aud": "calendar-app",
  "exp": 1767225600,
  "iat": 1767222000,
  "nonce": "n-0S6_WzA2Mj",
  "email": "[email protected]",
  "email_verified": true
}
Token Who it is for What it proves
ID token The client application Who logged in, when, and for which app
Access token The API Permission to call it with certain scopes

Mixing these up is a classic, serious bug. An API must not accept ID tokens as if they were access tokens (the ID token was minted for the client, not the API, and its aud says so), and a client must not treat an access token as proof of identity (an access token is an opaque capability, not a claim about a user). The aud claim is what keeps them in their lanes, which is why checking it is non-negotiable.

Validating an ID token

Before trusting the login, the client must check:

  1. The signature, using the provider’s keys fetched from its jwks_uri (and cached).
  2. iss equals the expected provider exactly.
  3. aud contains this client’s id.
  4. exp is in the future.
  5. nonce matches the value the client generated and sent at the start of the flow. This is the OIDC replay defense: a stolen or replayed ID token carries the wrong nonce and is rejected. For account linking, also require email_verified to be true before trusting email.

Providers publish everything at /.well-known/openid-configuration (endpoints, supported algorithms, the jwks_uri). Reading it during recon reveals the flows and signing algorithms in use.

SAML: the enterprise veteran

SAML predates OIDC and carries identity in signed XML rather than JSON. The flow is similar in spirit:

  1. You open an application, the service provider (SP).
  2. It redirects you to the identity provider (IdP) with a SAML request.
  3. You authenticate. The IdP posts a signed XML assertion back to the SP through your browser.
  4. The SP validates the signature and conditions, then logs you in.

Why SAML was so fragile: XML signature wrapping

XML digital signatures are notoriously hard to validate correctly, and the reason is worth understanding because it is a general lesson about parsing. An XML signature covers a specific element, identified by an id. The vulnerability, XML Signature Wrapping (XSW), exploits a split between the component that verifies the signature and the component that reads the identity: the verifier confirms a validly signed element exists somewhere in the document, while the application logic separately walks the tree and reads the user from a different element the attacker injected. The signed element is genuine (copied from a real assertion), but the element the app actually trusts is forged and unsigned. Two parsers, two views of the same document, and the attacker lives in the gap, which is the same shape as SQL injection and XXE: disagreement between layers about what the bytes mean. Robust SAML libraries defend by validating that the signed element is the one being consumed, and by rejecting documents with duplicate ids or multiple assertions.

Other SAML pitfalls: accepting unsigned assertions, not pinning the IdP’s certificate, canonicalization quirks, and comment-injection tricks that split a value like an email during parsing.

Where SSO goes wrong

Problem Effect
Not validating aud A token issued for one app is accepted by another
Trusting email without email_verified for account linking Takeover by registering an unverified email at a permissive provider
Accepting unsigned or weakly signed assertions, or XSW Forged logins
Multi-tenant apps accepting tokens from any tenant Any organization’s users log in to yours
Missing nonce check (OIDC) ID token replay
No single logout propagation Logging out of the provider leaves app sessions alive

Field notes: breaking SAML

SAML assertions are signed XML, and the signature is where it breaks. Load the flow in Burp with the SAML Raider extension, which decodes the base64 assertion and lets you edit and re-sign it:

  • Signature stripping. Remove the <Signature> element entirely and submit. If the Service Provider accepts an unsigned assertion, you edit the NameID to any user and log in as them.
  • XML Signature Wrapping (XSW). Keep the original signed assertion but add a second, unsigned assertion (or move the signed one into a wrapper) so the verifier checks the signed element by id while the application reads your injected one. SAML Raider automates the eight standard XSW permutations. This is the same mechanism as SOAP signature wrapping.
  • XML comment truncation. Insert a comment inside the NameID: [email protected]<!---->.attacker.com. Some XML libraries return only the text before the comment to the application, so the signature covers the whole string but the app reads [email protected]. This was a real, widespread SSO bypass.
  • Key confusion / self-signed cert. Re-sign the assertion with your own certificate and submit it along with that cert; an SP that trusts the embedded certificate rather than a pinned IdP key accepts it.

Field notes: OIDC and token confusion

On the OpenID Connect side, the bugs cluster around trusting the wrong token or claim:

  • id_token vs access_token mixups: an API that accepts an id_token (meant for the client) as a bearer credential, or vice versa, is confused-deputy territory. Check the aud (audience) claim is validated so a token minted for one client cannot be replayed at another.
  • Missing nonce or state validation allows replay and login CSRF, exactly as in OAuth.
  • Issuer confusion: if the SP does not pin the iss and the signing key together, a token from an attacker-controlled IdP with the right sub/email logs you in.
  • email_verified ignored: linking accounts on an unverified email is account takeover.

Test SSO only in environments you are authorized to assess, and never submit real assertions to third-party identity providers you do not own.

Testing SSO safely

Stand up a free developer tenant at Okta, Auth0, or Microsoft Entra, register a test application, and inspect every ID and access token with the JWT decoder. For SAML, the SAML Raider extension for Burp Suite decodes and rewrites assertions so you can probe signature validation in a lab you control. Only ever against tenants and apps you own.