OpenID Connect and SAML

OIDC layers identity on OAuth; SAML is the older enterprise SSO. When to pick each, and why both still ship in 2026.

Building Block Intermediate
11 min read
oidc saml sso identity authentication

What it is#

OpenID Connect (OIDC) and SAML are the two protocols that solve federated identity: letting a user log into one application using credentials managed by a different system (Okta, Google Workspace, Azure AD, ADFS, Auth0). Both predate the modern API-design era; both ship in production in 2026; they have very different shapes.

  • OpenID Connect is OAuth 2 plus identity. RFC-spec’d by the OpenID Foundation in 2014. The flow is identical to OAuth 2’s Authorization Code grant — the additional contract is an id_token (a JWT) returned alongside the access token, plus a UserInfo endpoint for fetching claims about the authenticated user. JSON on the wire. Modern web stack.
  • SAML 2.0 is the older enterprise standard, ratified by OASIS in 2005. XML-based; SOAP-adjacent; rich in profiles and bindings. The flow is built around assertions — signed XML documents that say “this user is alice@acme.com and these are her group memberships, signed by the Identity Provider.” Browser-based POST or redirect bindings carry assertions between the Identity Provider and the Service Provider.

Both protocols share the same actors: an Identity Provider (IdP) that authenticates users, a Service Provider (SP) (or Relying Party in OIDC terminology) that wants to know who the user is, and the user in the middle. Both rely on signed tokens to prove the IdP vouched for the identity claim. The difference is everything below that level — wire format, ecosystem, tooling, complexity.

When to use it#

  • Reach for OIDC for modern consumer logins (“Sign in with Google / Apple / Microsoft”), B2B SaaS that needs SSO with a small number of enterprise customers, and any new identity integration in 2026. JSON, JWTs, well-tooled in every language.
  • Reach for SAML when an enterprise customer mandates it — government agencies, large financial institutions, and any organisation running Okta / OneLogin / Ping / ADFS will often require SAML for procurement reasons. New SaaS products typically ship SAML support somewhere between their first 10 and first 50 enterprise customers.
  • Ship both if you sell to enterprises. Okta and Auth0 are full businesses built around abstracting away the OIDC-vs-SAML choice for SaaS vendors — you integrate with them once, they speak SAML to the enterprise IdP and OIDC to your app.

The decision rule for greenfield is OIDC unless a partner mandates SAML. The decision rule for existing enterprise software is whatever the customer’s IdP speaks — fighting the IdP is a losing battle.

How it works#

The two protocols solve the same problem in very different shapes.

OIDC: Authorization Code flow + id_token#

OIDC reuses OAuth 2’s Authorization Code grant verbatim. The Relying Party redirects the user to the IdP’s /authorize endpoint with scope=openid (plus optionally email, profile) — the openid scope is what tells the IdP “this is an identity request, return an id_token”. After the user authenticates, the IdP issues an authorisation code; the RP exchanges it at /token and gets back:

OIDC token response
{
"access_token": "eyJ...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile"
}

The id_token is a JWT — three base64url-encoded parts (header.payload.signature) separated by dots. The payload is the identity claim:

Decoded id_token payload
{
"iss": "https://accounts.example.com",
"sub": "248289761001",
"aud": "client_abc",
"exp": 1717003600,
"iat": 1717000000,
"nonce": "n-0S6_WzA2Mj",
"email": "alice@example.com",
"email_verified": true,
"name": "Alice Example"
}

The RP must verify the id_token before trusting it:

  1. Fetch the IdP’s signing keys from its /.well-known/openid-configuration document (the JWKS URI).
  2. Verify the JWT signature using the public key matching the kid in the JWT header.
  3. Check iss matches the expected IdP, aud includes the RP’s client_id, exp is in the future, iat is not too far in the past, and nonce matches the value the RP sent in the initial request.

A library handles all of this in five or six lines of real code, but skipping any single check breaks the security model — id_token verification gaps are a recurring CVE source.

SAML: SP-initiated, POST binding#

The most common SAML flow is SP-initiated with HTTP-POST binding:

User-Agent Service Identity
(browser) Provider Provider
│ │ │
│ 1. visit app │ │
│─────────────────►│ │
│ 2. 302 redirect with │
│ SAMLRequest (deflated, base64) │
│◄─────────────────│ │
│─────────────────────────────────────►│
│ 3. user authenticates at IdP │
│ 4. IdP returns HTML form with hidden │
│ SAMLResponse field, auto-submit │
│◄─────────────────────────────────────│
│ 5. POST SAMLResponse to SP's ACS URL │
│─────────────────►│ │
│ 6. SP validates assertion, sets │
│ session cookie, redirects to app │
│◄─────────────────│ │

The SAML assertion inside the SAMLResponse is an XML document containing:

Simplified SAML assertion
<saml:Assertion ID="..." IssueInstant="2026-05-30T08:14:23Z">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<ds:Signature>...</ds:Signature>
<saml:Subject>
<saml:NameID>alice@example.com</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData Recipient="https://sp.example.com/acs"
NotOnOrAfter="2026-05-30T08:19:23Z"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2026-05-30T08:14:23Z"
NotOnOrAfter="2026-05-30T08:19:23Z">
<saml:AudienceRestriction>
<saml:Audience>https://sp.example.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AttributeStatement>
<saml:Attribute Name="groups">
<saml:AttributeValue>engineering</saml:AttributeValue>
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>

Validation rules are denser than OIDC: verify the XML signature with XML Digital Signature rules, check Audience, Recipient, time bounds, replay protection (the SP must reject duplicate AssertionID values within their validity window), and ensure the signature covers the parts of the document it claims to cover.

XML signature wrapping (XSW) attacks have hit major SAML implementations because the XML signature is on a subtree, and a naive parser may verify the signature against one node and then read attributes from a different node. The fix is a careful library — never roll your own XML signature verification.

Verifying an OIDC id_token in three languages#

The minimum-viable verification step that an RP does on every login.

Verify id_token — Python
# pip install python-jose[cryptography] requests
from jose import jwt
import requests
ISSUER = "https://accounts.example.com"
CLIENT_ID = "client_abc"
def verify_id_token(token: str, expected_nonce: str) -> dict:
jwks = requests.get(f"{ISSUER}/.well-known/jwks.json", timeout=5).json()
claims = jwt.decode(
token, jwks,
algorithms=["RS256"],
audience=CLIENT_ID,
issuer=ISSUER,
)
if claims.get("nonce") != expected_nonce:
raise ValueError("nonce mismatch")
return claims # claims["sub"], claims["email"], etc.

SAML verification is conceptually similar but mechanically heavier — XML canonicalisation, signature wrapping defences, replay caches. Use a library (python-saml, gosaml2, node-saml); do not parse SAML assertions by hand.

Variants#

The major shapes within each protocol family:

VariantFamilyWhen it fits
OIDC Authorization Code + PKCEOIDCThe modern default for browser and mobile clients.
OIDC ImplicitOIDCDeprecated. Don’t ship.
OIDC Hybrid FlowOIDCRare, mostly for compatibility with old SP-initiated flows.
SAML SP-Initiated POSTSAMLThe most common SAML flow. User visits SP first.
SAML IdP-InitiatedSAMLUser starts at the IdP (a portal); IdP launches the SP. CSRF concerns are real.
SAML HTTP-Redirect BindingSAMLSmaller assertions go through URL parameters.
OAuth 2 + JWT-shaped access tokens (RFC 9068)OAuth+Access tokens that the resource server can verify locally, like an id_token. Not OIDC, but uses the same JWT machinery.
WS-FederationEnterpriseOlder Microsoft-led SSO protocol, mostly seen with on-prem ADFS.

Trade-offs#

OIDC strengths. JSON wire format; JWT signing is well-tooled across every language; small spec surface (one core spec, a handful of optional extensions); discovery is built in (.well-known/openid-configuration); composes with OAuth 2 access tokens; widely supported by consumer IdPs (Google, Apple, Microsoft). Costs: token-storage and refresh-rotation patterns still require care; JWT verification gaps are a recurring CVE source.

SAML strengths. Deeply deployed in enterprise IT; IdP-side group/role mapping is rich; signed assertions are self-contained; predates and survives every framework cycle. Costs: XML and XSLT and XML Signature; large spec surface (multiple bindings, profiles, NameID formats); homemade implementations have a CVE history (XSW, signature stripping); developer hostility is real.

What both share:

  • Both rely on a signing key managed by the IdP. Rotation of that key must be planned; both protocols define a key-discovery mechanism (JWKS URI for OIDC, SAML metadata document for SAML).
  • Both produce a session at the SP after the federated flow completes. The IdP step is one-time; subsequent calls use a local session cookie or local access token.
  • Both are browser-based. Neither protocol fits server-to-server identity directly — for service identity, use mTLS or OAuth 2 Client Credentials.

Common pitfalls#

  • Confusing OAuth 2 and OIDC. Access tokens are not identity. If your “login” endpoint reads access_token and treats access_token.sub as the user, you have a confused-deputy bug. Use the id_token.
  • Skipping nonce in OIDC. The RP generates a nonce, sends it in /authorize, expects it back in the id_token. Without this check, an attacker can replay an old id_token from another session.
  • Trusting the email claim in id_token without email_verified. Some IdPs let users set their email without verifying it. If you tie account access to email, check the verified flag.
  • Hard-coding JWKS keys. Keys rotate. Always fetch the JWKS dynamically and cache with a short TTL (5-15 minutes), or honour the kid header on every token.
  • Forgetting algorithm pinning. Many JWT libraries default to “verify with whatever algorithm the header says”. An attacker who can swap alg: RS256 for alg: none (or alg: HS256 against the public key) bypasses signature verification. Pin the expected algorithm explicitly.
  • Rolling your own SAML XML signature verification. XSW attacks are subtle and well-documented. Use a library; never reimplement.
  • Long-lived SAML assertion windows. The NotOnOrAfter time should be minutes, not hours. Bearer assertions stolen in transit can be replayed up to that bound.
  • IdP-initiated SAML without CSRF protection. Without a RelayState that ties the SAML response to a prior browser session, an attacker can deliver an attacker-controlled SAML response and log the user in as themselves. SP-initiated is safer; IdP-initiated needs extra care.
  • Mixing same-origin and SAML. Some SAML libraries set the session cookie without SameSite=Lax, breaking cross-site logins from the IdP. The fix is SameSite=Lax and accepting the redirect-back constraint.
  • No clock skew tolerance. OIDC and SAML both check time bounds. A 30-second skew between IdP and SP fails every login. Allow ~60s of skew.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.