~/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.
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
openidscope. - 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:
- The signature, using the provider’s keys fetched from its
jwks_uri(and cached). issequals the expected provider exactly.audcontains this client’s id.expis in the future.noncematches 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 requireemail_verifiedto be true before trustingemail.
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:
- You open an application, the service provider (SP).
- It redirects you to the identity provider (IdP) with a SAML request.
- You authenticate. The IdP posts a signed XML assertion back to the SP through your browser.
- 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 theNameIDto 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_tokenvsaccess_tokenmixups: an API that accepts anid_token(meant for the client) as a bearer credential, or vice versa, is confused-deputy territory. Check theaud(audience) claim is validated so a token minted for one client cannot be replayed at another.- Missing
nonceorstatevalidation allows replay and login CSRF, exactly as in OAuth. - Issuer confusion: if the SP does not pin the
issand the signing key together, a token from an attacker-controlled IdP with the rightsub/emaillogs you in. email_verifiedignored: 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.