SSO
One identity provider authenticates the user; many applications trust it. The security argument is not convenience — it is that credentials, MFA policy and offboarding live in one place. Disable an account at the IdP and access ends everywhere, immediately.
The vocabulary, which differs by protocol
| Role | SAML | OIDC |
|---|---|---|
| Authenticates the user | IdP | OpenID Provider (OP) |
| The application | Service Provider (SP) | Relying Party (RP) |
| What it receives | XML assertion | ID token (JWT) |
Same idea, different words. An interviewer switching between “SP” and “RP” is usually checking whether you notice they are the same role.
The flow, once
- User hits the app; no session, so redirect to the IdP.
- User authenticates at the IdP (MFA happens here, not in your app).
- IdP redirects back with a code or assertion.
- App validates it, then creates its own session.
- The next app redirects to the same IdP, which already has a session, and returns immediately. No second login — that is the SSO.
Step 4 is the whole security boundary, and it is where implementations fail.
Validating an OIDC callback
from authlib.jose import JsonWebToken, JWTClaims
def verify(id_token: str, nonce: str) -> JWTClaims:
claims = JsonWebToken(["RS256"]).decode(
id_token,
# fetched, cached, rotated
key=jwks,
claims_options={
"iss": {"essential": True, "value": ISSUER},
"aud": {"essential": True, "value": CLIENT_ID},
},
)
claims.validate(leeway=60) # exp, iat, nbf
if claims["nonce"] != nonce: # replay defence
raise BadNonce
return claimsEach line is a real attack:
issandaud— without them, a token the IdP issued for a different application is accepted by yours. This is token substitution, and it is the most common SSO vulnerability.- Algorithm allow-list (
["RS256"]) — omit it and a token withalg: noneor a symmetric algorithm using the public key as the HMAC secret validates. leeway— clock skew between your host and the IdP rejects valid tokens otherwise. A minute is normal; hours means your NTP is broken.nonce— bound to the session that started the flow, so a captured token cannot be replayed into a different browser session.
And the redirect URI must be an exact match against an allow-list, not a prefix:
ALLOWED = frozenset({
"https://app.example.com/auth/callback",
"https://staging.example.com/auth/callback",
})
def check(uri: str) -> str:
# exact, not startswith
if uri not in ALLOWED:
raise BadRedirect(uri)
return uristartswith("https://app.example.com") accepts
https://app.example.com.evil.com/cb, and the IdP will happily deliver the code
there. Set membership has no such failure mode.
SAML’s equivalent, and its extra hazard
SAML validates a signature over XML rather than a JWT, which brings a problem JSON does not have: XML Signature Wrapping. An attacker moves the signed assertion into a decoy element and adds an unsigned one, and a parser that reads “the assertion” rather than “the assertion that was signed” accepts it.
The defence is a library that resolves the signature reference properly, not your own XML handling. This is the single strongest argument for never writing SAML parsing yourself — see SSO Attack Vectors.
Provisioning and deprovisioning
JIT provisioning creates the local account on first login from IdP attributes. It is convenient and it is only half a lifecycle: nothing deletes the account when the user leaves, because they simply stop logging in.
SCIM is the other half — the IdP pushes creates, updates and deletes to your app. If the interview is about a regulated environment, deprovisioning is the question they care about, and “JIT for creation, SCIM for the lifecycle” is the answer.
Deep dives
- SAML 2.0 Deep Dive — assertion structure, signing, bindings
- OIDC and OAuth 2.0 Flows — auth code + PKCE, discovery, JWKS
- Tokens: ID, Access, Refresh — what each token is for
- Session Management with SSO — token vs cookie sessions, sliding expiry
- Single Logout (SLO) — front-channel vs back-channel SLO
- SCIM and User Provisioning — JIT vs SCIM, deprovisioning
- Group, Role, and Claim Mapping — claims to app permissions
- SSO Attack Vectors — Golden SAML, XSW, replay, IdP-initiated risks
- MFA and Step-Up Authentication — second factor, step-up auth, ACR
- Multi-Tenant SSO — per-tenant IdP, home realm discovery
- Python Libraries for SSO — Authlib, python3-saml, FastAPI integration
- SSO Common Pitfalls — clock skew, cert rotation, audience, redirect URI
Interview angle 6
- “What does SSO actually give you?” - one identity provider authenticates for many applications, so credentials, MFA policy and deprovisioning live in one place. The security win is centralised offboarding: disable once, access ends everywhere.
- “SAML or OIDC?” - OIDC for anything new: JSON and JWT over HTTP, simpler to implement, native to mobile and SPAs. SAML is XML-based and entrenched in enterprise, so you will meet it whether or not you would choose it.
- “What exactly do you validate on the token?” - issuer and audience (or a token minted for another app is accepted by yours), the signature against the IdP’s JWKS with an explicit algorithm allow-list, expiry with a small leeway for clock skew, and the nonce against the session that started the flow.
- “Why does the algorithm allow-list matter?” - without it a token can arrive specifying
none, or a symmetric algorithm that uses the IdP’s public key as an HMAC secret. Both validate against a naive implementation. - “What breaks in SSO integrations?” - clock skew invalidating assertions, certificate rotation on the IdP side, incorrect audience or issuer validation, redirect URIs matched by prefix instead of exactly, and group-to-role mapping drifting from what the IdP sends.
- “JIT provisioning is enough, isn’t it?” - it covers creation only. Nobody is deleted when they leave; they just stop logging in. SCIM pushes the full lifecycle including deprovisioning, which is the half an auditor asks about.