Blogs

The Auth Protocol Map: OAuth2 vs OIDC vs SAML vs LDAP vs SCIM

2026-06-2714 min read

Five acronyms that all show up in login conversations and do completely different jobs. The one distinction that untangles them, and where each one actually fits.

The Auth Protocol Map: OAuth2 vs OIDC vs SAML vs LDAP vs SCIM

This is a companion to my series on what public-private keys actually do. That series is about the crypto primitives. This post is one level up: the protocols that produce and exchange the signed tokens those primitives secure. If the signing post explained the JWT, this one explains where the JWT comes from.

Why these five blur together

OAuth2, OIDC, SAML, LDAP, SCIM. They all surface the moment you touch "login", so people treat them as interchangeable buzzwords. They are not. They solve different problems, and the single most common mistake (one that caused real security bugs across the industry for years) is assuming OAuth2 logs users in. It does not, on its own. Get the distinctions straight and the whole "auth stack" stops being alphabet soup.

The one distinction that unlocks everything

Two words that sound identical and mean opposite things:

  • Authentication (authN): who are you? Proving identity. Logging in.
  • Authorization (authZ): what are you allowed to do? Granting access.

Almost every confusion here is a authN/authZ mix-up. The headline fact:

OAuth2 is an authorization protocol. OIDC is a thin authentication layer bolted on top of it. They are not competitors; OIDC is OAuth2 plus identity.

Hold that and the rest falls into place.

The five, at a glance

StandardForFormatWhere you meet it
OAuth 2.0authorization: "let app X access resource Y on my behalf"access tokens"Allow this app to read your Google Drive"
OIDC (OpenID Connect)authentication: who is the user, an identity layer on top of OAuth2ID token (JWT) + JWKS + discoverymodern "Log in with..." / SSO
SAMLauthentication / SSOsigned XML assertionsolder, enterprise SSO (Okta, AD FS, corporate logins)
LDAPdirectory: query users / groupsa query protocol, not tokenslegacy apps authenticating against a directory
SCIMprovisioning: sync / auto-create usersJSON / RESTauto-add or disable employees across your apps

Notice they split into three jobs, not one: logging in (OIDC, SAML), delegating access (OAuth2), and user lifecycle (LDAP for lookup, SCIM for provisioning).

Where each protocol sits: authorization, authentication, directory, and provisioningWhere each protocol sits: authorization, authentication, directory, and provisioning

OAuth 2.0: delegated authorization

OAuth2 answers: how do I let one app act on my behalf at another, without giving it my password? The classic screen is "Allow Figma to access your Google Drive". You are not logging into Figma as Google here; you are granting Figma limited access to a Google resource.

The output is an access token: a credential the app sends to an API to prove "I am allowed to do this specific thing". It may be opaque (a random string the API looks up) or a JWT.

What OAuth2 deliberately does not do is tell the app who you are. There is no standard "identity" in raw OAuth2. For years people abused the access token as a stand-in for identity ("I can call the userinfo API, so the user must be logged in"), and it was fragile and exploitable. That gap is exactly what OIDC was created to fill.

OIDC: authentication on top of OAuth2

OpenID Connect takes the OAuth2 machinery and adds a real identity layer:

  • An ID token: a signed JWT that says who the user is (sub, email, iss, aud, exp). This is the exact token dissected in the signing post.
  • A userinfo endpoint for additional profile claims.
  • JWKS (the public keyring) and discovery (/.well-known/openid-configuration) so any client can find the issuer's endpoints and verify tokens automatically.

This is what every modern "Log in with Google / Microsoft / GitHub" button speaks, and what an identity provider like Authentik, Clerk, or WorkOS hands your backend. The trust in that ID token is pure signing: your backend verifies the JWT's signature against the issuer's JWKS, checks iss/aud/exp, and now knows who logged in.

Three tokens people constantly confuse

OAuth2/OIDC hand you up to three tokens, with different jobs:

TokenQuestion it answersTypical formLifetime
ID token (OIDC)who is the user?JWTshort
access token (OAuth2)what may the bearer do?opaque or JWTshort (minutes to ~1h)
refresh tokengive me a new access tokenopaque, stored securelylong

The ID token is for your app to learn the user's identity. The access token is for calling APIs. The refresh token silently buys new access tokens so the user is not bounced to a login screen every hour.

The OIDC login flow you actually use

Almost every OIDC login is the authorization-code flow. Five steps, and the last one is where the crypto from the signing post pays off:

The OIDC authorization-code flow, ending in a JWKS-verified ID tokenThe OIDC authorization-code flow, ending in a JWKS-verified ID token

  1. Your app redirects the browser to the identity provider's authorize endpoint.
  2. The user authenticates there (password, Google, a passkey, whatever the IdP offers).
  3. The IdP redirects back to your app with a short-lived code.
  4. Your backend exchanges that code (plus its client secret) for the ID token + access token at the token endpoint.
  5. Your backend verifies the ID token's signature against the IdP's JWKS, checks iss/aud/exp, and trusts the identity.

Step 5 is the entire bridge to the crypto series: the IdP signed the ID token with its private key; you verify with its public key. No call back to the IdP per request, no shared secret, just a signature check.

The five steps are the happy path. The interesting, bug-prone details live in the gaps, and they are exactly the things an auth SDK quietly handles for you (and that you have to get right if you ever hand-roll it).

The code-for-tokens exchange is a back-channel call

Step 4 deserves slowing down on. When the browser lands on your callback with ?code=...&state=..., your backend (not the browser) makes a server-to-server POST to the IdP's token endpoint, sending code + client_id + client_secret + redirect_uri, and gets back the ID token and access token.

Two things make that safe:

  • The code is single-use and useless on its own. Intercepting it does not help an attacker, because redeeming it also requires the client secret.
  • The client secret never touches the browser. It lives only on your server, and it is what proves "this really is my app" during the exchange.

That client-secret story is the confidential client case: an app with a backend that can actually keep a secret. Public clients (single-page apps, mobile apps, CLIs) cannot hide a secret, so they use PKCE (Proof Key for Code Exchange) instead. At the start the client invents a random code_verifier, sends only its SHA-256 hash (code_challenge) on the authorize request, and then proves possession by sending the original code_verifier at the token exchange. A stolen code is useless without the verifier. Current best practice (the OAuth 2.0 Security BCP, RFC 9700, 2025) recommends PKCE for all clients, secret or not, and it is exactly what CLI tools lean on, which is a post of its own.

And those tokens are signed by the IdP's private key, not yours. Your app has no signing key; the only key it ever touches is the IdP's public key from the JWKS, used to verify (the whole point of the signing post).

state: the anti-CSRF nonce that pulls double duty

Before redirecting to the IdP, your app generates a random state (e.g. secrets.token_urlsafe(16), a cryptographically random URL-safe string), stashes it in a short-lived cookie, and includes it on the authorize request. The IdP echoes state back on the callback, and you check it matches.

Its job is CSRF protection, not routing. An attacker cannot forge a valid callback because they cannot guess your state; without it, someone could trick your app into completing a login the user never started (login CSRF). In the minimal case it is purely a random nonce you compare for equality. But state has a second, optional job we will use below: it can also carry the URL the user originally wanted.

Once you have verified the ID token, you create a session and hand it to the browser as a cookie. The flags on that cookie decide how safe it is:

FlagWhat it doesWhy it matters
HttpOnlyJavaScript cannot read it (document.cookie is blind to it)stops an XSS bug from stealing the session
Secureonly sent over HTTPSstops it leaking over plain HTTP (omit on http://localhost, always set in prod)
SameSitecontrols sending on cross-site requestsCSRF defense, covered next
Max-Age / Expireslifetimeshort (minutes) for the state cookie, longer for the session
Domain / Pathwhich hosts and paths receive itscope; default is the exact host

Two that get conflated: HttpOnly is about XSS (script cannot read the cookie), Secure is about the network (cookie only crosses HTTPS). They defend against different attacks; in production you usually want both. And on lifetimes, max_age=300 is 300 seconds, five minutes, not five hours; the state cookie only needs to survive one login round-trip.

SameSite: Lax vs Strict vs None

This is the cookie flag people get wrong most, and it is what makes OAuth both work and stay safe. SameSite controls whether the browser attaches the cookie when a request originates from a different site:

ValueSent on cross-site requests?Trade-off
Strictnever, not even when you click a link to the site from elsewheresafest against CSRF, but breaks "click the email link and you are already logged in"
Laxonly on top-level navigations (a GET from clicking a link or following a redirect), not cross-site POST, fetch, or iframesthe sweet spot, and what OAuth needs
Nonealways, including cross-site iframes and AJAXrequired for genuine third-party / embedded contexts, and the browser rejects it unless Secure is also set

Two clarifications that fix the usual misconceptions:

  • SameSite is about cross-site (a different registrable domain, evil.com vs myapp.com), not sub-domains. app.myapp.com and myapp.com are the same site here. (One subtlety: modern browsers also fold the scheme into "site", so http:// and https:// of the same host count as different sites.)
  • Lax is why OAuth works, with a caveat. The standard callback uses response_mode=query: the IdP 302-redirects the browser back to you, a top-level GET navigation, so a Lax cookie (your state) rides along; a cross-site POST or hidden iframe would not carry it, which is what defeats CSRF. The caveat: if you use response_mode=form_post (or SAML's HTTP-POST binding), the callback is a cross-site POST, and a Lax cookie is not sent. Those flows need SameSite=None; Secure. (Chrome once had a "Lax + POST" two-minute grace window, but it only ever applied to cookies left implicitly Lax and is being removed, so do not rely on it.) Strict can also be too aggressive and drop the cookie on the inbound redirect; Lax is the right default for a standard OAuth flow.

Here is a gap most minimal demos skip. Say a logged-out user hits myapp.com/dashboard/conversation-123. Your guard bounces them to /login, they authenticate, and a naive callback finishes with redirect("/"), dumping them on the home page instead of the conversation they were after.

The fix is state's second job. At /login, capture the path the user originally wanted and carry it through, either encoded inside state or in a separate short-lived cookie. On /callback, after verifying, redirect to that saved path instead of /:

/login    : save original path "/dashboard/conversation-123" into state (or a cookie)
IdP       : authenticates, echoes state back
/callback : verify state + token, then redirect(saved_path) instead of "/"

This is exactly what production SDKs do; for example authkit-nextjs seals a returnPathname into its state. So state ends up doing double duty: a CSRF nonce (always) plus a return-path carrier (optional).

One security catch worth its own line: validate that return path before redirecting to it. Only allow same-origin relative paths. If you blindly redirect to whatever was stashed, an attacker can set it to https://evil.com and turn your login into an open redirect.

SAML: the XML elder

SAML does the same job as OIDC (authentication / SSO) but predates it and speaks signed XML assertions instead of JWTs. It is heavier and more verbose, and it is everywhere in enterprise: Okta, AD FS, corporate "log in with your company account" flows.

Conceptually it is the same primitive underneath, a signed statement of identity that a relying party verifies. The wire format is just XML with XML-DSig signatures rather than a compact base64 JWT. If you are building consumer or modern B2B login, you reach for OIDC; if you are selling to enterprises whose IT standardised on SAML years ago, you support SAML too.

LDAP: the directory, not a login protocol

LDAP is the odd one out: it is not about tokens at all. It is a protocol for querying a directory of users and groups. The canonical example is Microsoft Active Directory. Legacy and on-prem apps often "authenticate" by binding against an LDAP directory (checking a username/password directly against it) and reading group membership for authorization.

Think of LDAP as the database-of-people that older auth systems sit on top of, where OIDC/SAML are the modern, token-based handshakes that usually front a directory like it.

SCIM: provisioning, not login

SCIM is also not a login protocol. It answers a different lifecycle question: how do users get created, updated, and deactivated across all my apps automatically? It is a JSON/REST standard for provisioning.

The scenario: an employee joins, and your HR/identity system uses SCIM to auto-create their account in Slack, GitHub, and your internal tools; they leave, and SCIM deactivates all of them in one sweep. Login (OIDC/SAML) proves who someone is right now; SCIM keeps the set of accounts in sync over time. They complement each other.

Which one do you reach for?

You want to...Use
Add "Log in with Google/Microsoft" to a modern appOIDC
Let enterprise customers bring their own IdPSAML (and OIDC)
Let an app act on a user's behalf at an APIOAuth 2.0
Authenticate against an existing on-prem directoryLDAP
Auto-provision and de-provision users across appsSCIM

The takeaway

OAuth2 is authorization (delegated access). OIDC is authentication layered on OAuth2, and it is what mints the signed ID token your app trusts. SAML is the older XML way to do authentication/SSO. LDAP is a directory you query. SCIM is provisioning. Different jobs, often used together in one stack.

And every one of the identity protocols (OIDC's ID token, SAML's assertion) ultimately rests on a signature. That is the crux: to really understand them, understand digital signatures. Start from the overview if you want the crypto foundation first.