TL;DR

OAuth is authorization. OIDC is the identity layer on top of it. SAML is what your enterprise apps still speak. MFA is necessary but not sufficient — phishing-resistant factors (FIDO2, passkeys) are the only ones that actually stop the attacks that matter. Before Zero Trust networking can do anything useful, you need a working mental model for Zero Trust identity. This post is that model. Part 5 of seven; Part 4 left the “inside the tunnel is trusted” flaw untouched, and this is where we finally fix it.

This is a revised, written-down version of the AuthExperience talk I’ve given inside Tikal’s DevOps group and in private workshops since 2021. The deck circulated by word of mouth for years; this is the public, updated companion.

The two misconceptions that break every auth conversation

Before anything else, two claims that will save you an hour in every design review:

OAuth is not authentication. It is authorization. OAuth gives a client application a grant to access a resource on your behalf. It does not tell the client who you are. This is the single most common mistake in the ecosystem, and it’s the reason “Log in with Google” was broken for years until OIDC became the standard layer on top.

OIDC is the identity layer OAuth was missing. OpenID Connect (OIDC) is a thin, standardized layer over OAuth that adds an ID token — a signed JWT the client can actually verify to prove who logged in, when, and how. If OAuth is a temporary apartment key, OIDC is the photo ID that comes with it.

Everything else — SAML, PKCE, the device flow, IRSA, Workload Identity — is a variation, optimization, or legacy compat layer on these two ideas.

LDAP to federation: why this stack exists

In the pre-cloud world, every app had its own user database. Then came LDAP — one directory tree, one place to manage identities. That worked while the trust boundary was inside our building (Part 1’s idea, still paying dividends). The moment an organization started consuming third-party SaaS, two problems appeared: managing customer identities the company didn’t own, and onboarding users whose origin directory wasn’t yours. SSO (“use your corporate email”) was the first answer, “social login” was the second. Both are federation — outsourcing the identity question to someone who already knows the user. It’s the same centralization-then-delegation arc as DNS, package registries, and CDNs.

captionless image

The terminology alignment that everyone gets wrong

The protocols use different names for the same actors. Internalize this table once and every spec becomes readable, focusing on Auth 2.0, OIDC or SAML

captionless image

Every OAuth/OIDC/SAML doc you read will use one vocabulary. This table lets you translate between them on the fly.

The flow that matters: Authorization Code + PKCE

There are five OAuth flows in the spec. In practice, for anything running in 2026, you want Authorization Code with PKCE (pronounced “pixie”). It is the default for web apps, SPAs, mobile apps, and CLI tools. The other flows are either legacy (Implicit — deprecated, leaks tokens via URL fragment) or specialized (Client Credentials for M2M, Device Code for input-constrained devices like TVs).

The PKCE dance in four steps:

  1. The app generates a random code_verifier and its SHA-256 code_challenge, then redirects the browser to the IDP with the challenge.
  2. The IDP authenticates the user (password, MFA, passkey — whatever its policy is), gets consent, and redirects back with a short-lived authorization code.
  3. The app exchanges the code + the original code_verifier for an ID token and an access token at the token endpoint.
  4. The app verifies the ID token’s signature, iss, aud, exp, and nonce, and starts a session.

PKCE | Proof Key for Code Exchange (RFC 7636)

PKCE exists because the authorization code travels through the browser — which means through URL fragments, browser history, referer headers, and potentially a hostile extension. The code_verifier is the secret that never leaves the app, proving the token exchange came from the same client that started the flow.

// A decoded OIDC ID token payload — the things you actually validate
{
  "iss": "https://id.example.com/realms/lab",   // who issued it
  "sub": "a1b2c3-user-uuid",                    // stable user identifier
  "aud": "my-web-app",                          // who it was issued for
  "exp": 1735689600,                            // expiration (Unix epoch)
  "iat": 1735686000,                            // issued at
  "nonce": "n-0S6_WzA2Mj",                      // anti-replay, set by client
  "email": "[email protected]",
  "email_verified": true
}

If any of iss, aud, exp, or nonce fails validation, the login is rejected. Those four claims are the entire trust model in four lines.

SAML is not dead

Every shiny OIDC post forgets that SAML is still everywhere in the enterprise. Workday, Salesforce, ServiceNow, and half of your vendor procurement estate still speak SAML POST-binding in 2026. It’s XML, it’s operationally painful — but the install base is not going anywhere. Bridging SAML↔OIDC is what Keycloak, Auth0, and Okta earn their money doing.

🌐 DNS, again — where federation is discoverable

DNS keeps showing up in this series as load-bearing infrastructure, and identity is no exception. OIDC clients bootstrap themselves by fetching /.well-known/openid-configuration from the issuer’s DNS name — that document is how the client learns the authorization endpoint, token endpoint, JWKS URI, and supported algorithms. LDAP had the same idea a generation earlier with _ldap._tcp.<domain> SRV records. Email’s federated identity layer — SPF, DKIM, DMARC — is published entirely in DNS TXT records and is, quietly, the oldest federated identity system we still run at scale. None of this is trustworthy without DNSSEC underneath it, which is why Part 6 will promote DNS from discovery plane to policy enforcement plane.

MFA, and why “we require MFA” is not enough

“We require MFA” is the beginning of the conversation, not the end. The factor matters:

  • SMS / voice codes — phishable, SIM-swap-able. Banned by NIST for high-assurance use for years. Stop using them.
  • TOTP apps (Authenticator, Authy) — better, but still phishable. An attacker who tricks you onto id-exmaple.com captures the six digits and replays them in real time.
  • Push approvals (Duo-style) — better still. Prone to MFA fatigue — the “approve?” prompts someone clicks at 3am to make the phone stop.
  • FIDO2 / WebAuthn / passkeys — the only phishing-resistant factor. The browser binds the assertion to the origin. A phishing site gets no signature it can replay against the real site, because the real site’s origin is what gets signed.

If you move one thing in your security posture this year, it’s to make FIDO2 the baseline for admins and high-risk users. Everything else is a stepping stone.

captionless image

Where DevOps engineers actually meet this

The stack above is not abstract for a DevOps practitioner — it’s where ~/.aws/config, Workload Identity, GitHub Actions OIDC, and External Secrets all come from. A few cases worth naming:

  • AWS IAM role chaining via ~/.aws/config profiles with MFA-challenged session tokens — the classic “access key plus MFA code” dance.
  • IRSA (IAM Roles for Service Accounts) and Workload Identity on GKE — pods assume cloud roles based on a Kubernetes-issued OIDC token presented to the cloud’s STS. The cluster itself becomes an OIDC provider. No static keys.
  • GitHub Actions OIDC → AWS/GCP — the workflow is issued a short-lived OIDC token, the cloud trusts that token via a pre-registered OIDC issuer, and the workflow assumes a role with no long-lived secret in GitHub.
  • External Secrets Operator — reaches into Vault, AWS Secrets Manager, or GCP Secret Manager using workload identity, then syncs secrets into Kubernetes without a single static credential in the cluster.

Every one of these is the same pattern: issue a short-lived token, federate it across a trust boundary, let the receiver verify signature + issuer + audience. If you understand PKCE, you already understand IRSA.

An anchor from across engagements

I cannot point at one client here — this pattern is so consistent across consulting work that it is the generic story. The pattern repeats: platform teams arrive running a home-grown LDAP with a forest of static API keys checked into Kubernetes manifests, and leave running a federated IDP with FIDO2 for humans and OIDC-federated workload identity for workloads. Both halves are necessary. Humans with passkeys and workloads with long-lived AWS_SECRET_ACCESS_KEY is a half-finished migration, and it’s where the last two breaches I saw started.

captionless image

👐 Hands-on: Keycloak, PKCE, and a token you can decode

The full lab — a Keycloak instance on k3d, a protected demo app, end-to-end PKCE walkthrough, TOTP then WebAuthn enrollment, and a replay attempt to show what aud and nonce actually prevent — lives at [github.com/hagzag/the-road-2-zerotrust/tree/main/practice/part5](https://github.com/hagzag/the-road-2-zerotrust/tree/main/practice/part5).

The short version:

# From practice/part5/
./run.sh                          # create cluster, deploy Keycloak + demo app
./bootstrap-realm.sh              # import realm, register OIDC client
open http://localhost:8080/app    # click "login", watch PKCE in DevTools
./decode-token.sh <id_token>      # prints claims, verifies iss/aud/exp

The aha moment is opening the browser DevTools during the redirect and watching the code_challenge, code, and eventually the ID token flow through the query string in that exact order.

Where this leaves you ⁉️

Identity is the new perimeter — but a perimeter without an enforcer is just a label. OIDC tells you who the caller is. WebAuthn tells you they haven’t been phished. Neither decides, at the moment a specific packet hits a specific service, whether it’s allowed. That decision is what Zero Trust Networking adds, and in Part 6 we connect the identity plane from this post to the network plane from Parts 1–4.

📖 Further Reading