Security · Lesson 07
OpenID Connect & SAML
OAuth 2.0 answers "what may this app do?" — but it says nothing about who is logged in. OpenID Connect adds that identity layer. SAML solves the same problem with XML in enterprise environments. Both power the "sign in with…" buttons you use every day.
By the end you'll be able to
- Explain what OpenID Connect adds to OAuth 2.0 and what an ID token contains.
- Describe the SAML SSO flow and explain where it dominates over OIDC.
- Distinguish authentication (authN), authorization (authZ), and Single Sign-On (SSO) in one sentence each.
The missing piece in OAuth 2.0
Imagine you hand your valet key (from the previous lesson) to a parking attendant, and then you ask: "Did a human hand you this key, or was it passed through a relay?" The valet key says nothing about its owner — only what doors it can open. OAuth 2.0 has the same blind spot: an access token proves what a client may do, but not which human granted it, or even whether a human was involved at all.
OpenID Connect (OIDC) fills that gap by adding one new token — the ID token — and a standard way to ask "who is this user?" It is OAuth 2.0 underneath, with an identity contract bolted on top. You get both an access token (for API calls) and an ID token (for your app to know who logged in).
The ID token: a JWT about the user
An ID token is a JSON Web Token (JWT) signed by the identity provider. Its payload always includes a standard set of claims:
// ID token payload (decoded — the actual token is Base64URL-encoded)
{
"iss": "https://accounts.example.com", // issuer
"sub": "user_7f3a9c", // subject (stable user id)
"aud": "client_app_xyz", // audience — must match your client_id
"exp": 1750000000, // expiry (Unix timestamp)
"iat": 1749996400, // issued-at
"nonce": "k9sRt2pQ", // replay-attack protection
"email": "ada@example.com", // profile scope granted
"name": "Ada Lovelace"
}
Your app verifies the signature, checks the aud matches your own client ID, and confirms the token has not expired — then you know exactly who logged in. The access token is still used for API calls; the ID token is for your app's session. Never send the ID token to an API as a Bearer token — it is for the client, not the resource server.
SSO: one login, many apps
Single Sign-On (SSO) means logging in once to an identity provider and then gaining seamless access to multiple connected services without re-entering credentials. The identity provider authenticates you; each service trusts the IdP's assertion.
SAML: SSO the enterprise way
Security Assertion Markup Language (SAML 2.0) is an older XML-based standard that predates OAuth. It is still dominant in large enterprises because it integrates tightly with Active Directory / LDAP identity stores and legacy applications.
The flow is conceptually similar to OIDC: a Service Provider (the app) redirects the user to an Identity Provider (e.g. Okta, Microsoft AD FS), the IdP authenticates the user and returns a signed XML assertion to the SP, and the SP validates the assertion and creates a session.
| Feature | OpenID Connect (OIDC) | SAML 2.0 |
|---|---|---|
| Token format | JWT (compact, JSON) | XML assertion (verbose) |
| Transport | HTTP redirect + JSON body | HTTP redirect or POST with Base64 XML |
| Ecosystem | Modern web, mobile, SaaS | Enterprise, legacy apps, government |
| Built on | OAuth 2.0 | Own independent standard |
| Typical IdPs | Google, Auth0, Cognito, Okta (OIDC mode) | Okta (SAML mode), ADFS, Shibboleth |
| When to choose | New apps, APIs, mobile, B2C | Enterprise B2B, Active Directory integration |
Three concepts that interviewers conflate — the clean answer: Authentication (authN) = "prove who you are" (password check, biometric). Authorization (authZ) = "decide what you may do" (roles, scopes, object-level checks). SSO = "authenticate once, access many services without re-logging in." SSO is an application of authN delegated to a central IdP — it is not a separate type of security. OIDC and SAML are both protocols that implement SSO; OAuth 2.0 on its own is only authZ.
Using an ID token as a Bearer token for API calls. The ID token's aud claim is set to your client ID — not to the API. If you send it as a Bearer token, a correctly-implemented API will reject it because the audience does not match. Use the access token for API calls. Use the ID token only in your application to know who the user is.
Do always validate the nonce claim in OIDC responses to prevent replay attacks — your client generates a random nonce, includes it in the authorization request, and verifies it appears unchanged in the returned ID token. Don't skip this step just because the connection is over HTTPS; a stolen token from browser history or a log file can still be replayed without nonce validation.
Under the hood: how OIDC actually works
OpenID Connect is OAuth 2.0 authorization-code flow with two additions: the openid scope in the initial redirect, and an id_token returned alongside the access token. Everything else — PKCE, state, the token exchange — is identical to OAuth. The identity layer is thin; the transport layer is already OAuth.
# ① /authorize request — identical to OAuth but adds "openid" scope
GET https://accounts.example.com/authorize
?response_type=code
&client_id=my_app
&redirect_uri=https://app.example.com/callback
&scope=openid profile email // "openid" triggers OIDC; "profile"/"email" request claims
&state=r8tQmZ4k
&nonce=k9sRt2pQ // OIDC-specific: client stores this; IdP must echo it in id_token
&code_challenge=E9Melhoa2Ow...
&code_challenge_method=S256
# ② POST /token exchange (same as OAuth) returns TWO tokens:
HTTP/1.1 200 OK
{
"access_token": "eyJhbGci...", // for API calls
"id_token": "eyJhbGci...", // for your app to know who logged in
"token_type": "Bearer",
"expires_in": 3600
}
Decoded ID token and client-side validation
An ID token is a signed JWT. Its payload contains a fixed set of claims the client must validate in strict order before trusting the identity. Skipping any step leaves the app vulnerable to token substitution or replay.
# Decoded id_token payload (the actual token is three Base64URL segments joined by dots)
{
"iss": "https://accounts.example.com", // issuer
"sub": "user_7f3a9c", // stable, opaque user ID — use as primary key
"aud": "my_app", // MUST match your client_id exactly
"exp": 1750003600, // reject if exp < now()
"iat": 1750000000, // issued-at; reject if too far in the past
"nonce": "k9sRt2pQ", // MUST match the nonce the client sent in ①
"email": "ada@example.com",
"name": "Ada Lovelace",
"email_verified": true
}
# Client validation algorithm — in this exact order
1. Fetch the IdP's JWKS from /.well-known/openid-configuration → jwks_uri
2. Match the token's "kid" header claim against a key in the JWKS
3. Verify the signature using that public key and the pinned algorithm ("RS256" typical)
4. Assert iss == expected issuer URL (prevents cross-IdP token substitution)
5. Assert aud contains your client_id (prevents tokens meant for other apps)
6. Assert exp > now() (token not expired)
7. Assert nonce == nonce you stored in session before the redirect (replay protection)
8. Optionally assert iat is recent (e.g. within 5 minutes)
# Only after all 8 checks pass: trust the sub and create your app session
SAML assertion exchange at a protocol level
SAML uses XML POSTed through the browser as the transport mechanism. The browser never touches the assertion content — it just carries an opaque base64-encoded blob from the IdP to the Service Provider (SP). The core exchange has three steps:
The SAMLResponse is a base64-encoded, XML-signed document. It contains the user's NameID (usually an email or opaque ID), attribute statements (department, role, groups), and validity timestamps (NotBefore, NotOnOrAfter). The SP must validate the XML signature against the IdP's public certificate, check the NotOnOrAfter timestamp, and verify the Recipient matches its own ACS URL.
How to debug & inspect it
OIDC and SAML failures almost always fall into three categories: token validation errors, clock-skew issues, and configuration mismatches. Here is how to find each one in practice.
| Symptom | Protocol | Cause | Fix |
|---|---|---|---|
| "Invalid signature" / token rejected | OIDC | IdP rotated its signing key; client is caching the old JWKS or the wrong kid | Force a JWKS refresh; confirm the client looks up the key by kid, not by index |
| "aud mismatch" / "invalid audience" | OIDC | Token's aud is a different client_id; or you sent the ID token to an API that checks audience | Use the access token for API calls; ensure the ID token's aud equals your client ID |
| "iss mismatch" | OIDC | Multi-tenant IdP — token's iss contains a tenant ID your code doesn't account for, or you're comparing against the wrong base URL | Pin the expected issuer string exactly (including trailing slash or tenant path) |
| "nonce mismatch" | OIDC | Session was lost before callback, or the nonce was not included in the initial request | Ensure the nonce is stored in a session cookie before the redirect and read back in the callback handler |
| Token expired immediately | OIDC / SAML | Server clock skew — server is ahead of the IdP by more than the token's validity window | Synchronise clocks with NTP; add a ±30 s leeway in token validation |
| "SAML Response: Recipient mismatch" | SAML | The SP's Assertion Consumer Service URL in the SAMLResponse doesn't match the configured ACS URL | Register the exact ACS URL in the IdP; ensure no trailing-slash difference |
| "Signature verification failed" in SAML | SAML | IdP certificate expired or the SP is using an outdated certificate copy | Export the current IdP certificate from the IdP admin UI and re-import into the SP |
| SSO works but user attributes missing | SAML | IdP attribute mapping not configured — the assertion doesn't include the expected attribute statements | Configure attribute mappings in the IdP admin; confirm attribute names match what the SP expects |
OIDC / SAML debug checklist:
- Decode the id_token or SAMLResponse payload (shell commands above) — read the raw claims before touching library-level errors.
- Check
exp/NotOnOrAfteragainstdate -d @<timestamp>. If the token is already expired, the clock difference is the root cause, not the token itself. - For OIDC: confirm
issexactly matches your pinned issuer URL andaudexactly matches yourclient_id. - For OIDC: confirm the
noncein the token equals what you stored in the session before the redirect. - For signature errors: fetch the IdP's JWKS, find the key whose
kidmatches the token header, and verify it is current (not expired or replaced). - For SAML: decode the
SAMLResponsewithbase64 -d | xmllintand checkRecipient,NotOnOrAfter, and theIssuerelement.
🧠 Quick check
1. What does OpenID Connect add to OAuth 2.0?
OAuth 2.0 has refresh tokens and PKCE is layered on top separately. OIDC's distinctive addition is the ID token — a signed JWT that identifies the user — plus the /userinfo endpoint and standardized claims like sub, email, and name.
2. In which scenario would you choose SAML over OIDC?
SAML's XML-based assertion model and deep Active Directory / LDAP integration make it the default choice for enterprise SSO. Consumer apps and APIs are better served by OIDC's lighter JSON/JWT format. Machine-to-machine is OAuth client-credentials — no user, no SSO needed.
3. What is the purpose of the nonce claim in an OIDC ID token?
The client generates a random nonce and includes it in the authorization request. The IdP echoes it back in the ID token. The client verifies the returned nonce matches what it sent — so a token captured from one flow cannot be replayed to a different session.
✍️ Exercise: choose the right protocol
Three scenarios are described below. For each one, state which protocol you would use (OAuth 2.0 only, OIDC, or SAML) and give a one-sentence reason.
- A SaaS task-manager wants to add "Log in with GitHub."
- A healthcare network needs all 40 of its internal web portals to share a single login, backed by a Microsoft Active Directory forest.
- A payments service needs to call the fraud-scoring service with machine-level credentials at night, no user involved.
Model answer:
- OIDC — GitHub supports OpenID Connect; OIDC returns an ID token identifying the user, which is exactly what a "log in with" button needs. Plain OAuth 2.0 would give you an access token but no standard way to know who the user is.
- SAML — Microsoft Active Directory and ADFS have native SAML 2.0 support; integrating 40 internal apps via SAML federation is the standard enterprise pattern and avoids re-implementing directory sync across services.
- OAuth 2.0 client-credentials — no user is present, so SSO is irrelevant; client-credentials issues a scoped access token for the service account without requiring any user interaction or identity claims.
Rubric: Full marks for correct protocol per scenario with a clear rationale. Partial marks for correct protocol without explaining the "why." Bonus: noting that many enterprise IdPs (Okta, Azure AD) support both SAML and OIDC — the choice may come down to what the client library ecosystem supports.
Key takeaways
- OIDC adds an ID token and standard identity claims on top of OAuth 2.0 — it is the right choice for "log in with…" on modern web and mobile apps.
- The ID token is for your app to identify the user; the access token is for API calls. Never swap them.
- SAML is XML-based, predates OAuth, and dominates enterprise SSO tied to Active Directory.
- SSO is not a protocol — it is an outcome achieved through OIDC or SAML: one login grants access to many services.
- AuthN = who you are; AuthZ = what you may do; SSO = authenticate once, access many.