TECHNICAL PRESENTATION · IDENTITY SERIES

Introduction to
OpenID Connect

The identity layer that turned OAuth into "Sign in with…"
OIDC 1.0 ID token Discovery Federation 1.0 FAPI 2.0
πŸ‘€ User β†’ 🌐 RP (your app) β†’ πŸ” OP / IdP β†’ πŸͺͺ ID token β†’ βœ“ "logged in as Alice"

OAuth 2.0 (authorisation) plus a signed identity token = OIDC. Companion to Introduction to OAuth and OAuth for MCP.

Identity  Β·  ID Tokens  Β·  Discovery  Β·  Federation
01

Topics

Foundations

  • Why OIDC exists β€” authentication β‰  authorisation
  • 2014 β†’ 2025 history; OIDC vs OpenID 2.0 vs SAML
  • Roles & terminology β€” RP, OP, End-User
  • The relationship to OAuth 2.0

The protocol in detail

  • Authorisation Code + openid scope flow
  • ID token anatomy β€” header, payload, claims, signature
  • Validation checklist (the part everyone gets wrong)
  • Discovery, JWKS, UserInfo
  • Hybrid & response_type variants
  • Logout β€” RP-initiated, back-channel, front-channel

Modern OIDC

  • CIBA β€” phone-approval flows
  • SIOPv2 β€” Self-Issued OPs & verifiable credentials
  • OpenID Federation 1.0 (Sep 2024)
  • FAPI 2.0 β€” financial-grade profile

In practice

  • Real-world IdPs β€” Google, Entra, Apple, Okta, Auth0, Keycloak
  • OIDC vs SAML decision
  • Workload OIDC β€” GitHub Actions β†’ cloud STS
  • Common bugs and mitigations
02

Why OIDC Exists

OAuth 2.0 answers "may this app act on the user's behalf?" β€” it never says who the user is. Apps that wanted "sign in" had to abuse OAuth (e.g. fetch /me from Facebook and trust the result). OIDC turns OAuth's authorisation answer into a verifiable identity assertion.

Authentication vs authorisation

  • Authentication β€” proves who someone is. ("This is Alice.")
  • Authorisation β€” decides what they can do. ("Alice may read invoices.")
  • OAuth 2.0 = authorisation only. OIDC = authentication on top.
  • Your app needs both β€” and they are not interchangeable.

What OIDC adds to OAuth

  • ID token β€” a signed JWT with the user's identity
  • UserInfo endpoint β€” fetch profile attributes
  • openid scope β€” opt-in marker
  • Standard claims β€” sub, email, name, picture, …
  • Discovery β€” /.well-known/openid-configuration
  • Session management β€” logout, back-channel

"Sign in with…"

Every "Sign in with Google / Microsoft / Apple / GitHub" button you've ever clicked is OIDC. The standard turned what was a "social login" hack in 2010 into the default identity protocol of the modern web.

Why not just use OAuth?

  • OAuth access tokens are not identity assertions β€” they say "this client may call this API", not "this is user U"
  • Validating an access token gives you authorisation; only an ID token tells you who authenticated
  • OAuth has no standard way to express "I logged in 3 minutes ago" β€” OIDC does (auth_time)

Classic mistake

Using an OAuth access token as a login indicator. The OAuth deck (slide 15) has the full picture; this deck dives in to the consequences.

03

A Brief History

2005 2007 2014 2017 2022 2024 2025+ OpenID 1.0/2.0XML, redirect-based OAuth 1.0delegation, not identity OIDC Core 1.0final spec FAPI 1.0 + CIBAfinance-grade SIOPv2 draftverifiable creds OpenID Federation 1.0Sep 2024 FAPI 2.0,EU eIDAS / DPP Twenty years from "OpenID URLs" to a federated, JWT-based identity layer powering nearly every modern login.

OpenID 1.0/2.0 β€” the predecessor

  • 2005–2007: https://alice.myopenid.com as your identity
  • XML-based, redirect-driven, no JSON, no signed tokens
  • Famously confusing UX ("type your URL?")
  • Killed by social login; Yahoo & AOL never got it right

OIDC reset (2014) and beyond

  • Built on OAuth 2.0 + JWT β€” JSON-native, mobile-friendly
  • 2017: FAPI 1.0 β€” financial-grade APIs (Open Banking)
  • 2022: SIOPv2 β€” verifiable credentials, decentralised identity
  • Sep 2024: OpenID Federation 1.0 finalised β€” programmatic trust between OPs at scale
  • 2025+: FAPI 2.0 GA, EU eIDAS 2.0 / Digital Identity Wallet
04

OIDC = OAuth + Identity Token

OIDC is not a separate protocol β€” it's a profile of OAuth 2.0 plus a few small additions. Everything you know from the OAuth deck still applies; OIDC just changes what comes back.

A vanilla OAuth 2.0 request

GET /authorize?
  response_type=code&
  client_id=app123&
  scope=invoices:read&
  redirect_uri=https://app/cb&
  state=...&code_challenge=...&code_challenge_method=S256

# Token response
{ "access_token": "eyJ...",
  "token_type":   "Bearer",
  "expires_in":   3600 }

The same request as OIDC

GET /authorize?
  response_type=code&
  client_id=app123&
  scope=openid profile email&     # ← magic word
  redirect_uri=https://app/cb&
  state=...&nonce=abc123&            # ← required by OIDC
  code_challenge=...&code_challenge_method=S256

# Token response β€” now has an ID token
{ "access_token":  "eyJ...",
  "id_token":      "eyJ...",   # ← the identity assertion
  "token_type":    "Bearer",
  "expires_in":    3600 }

What changes

  • Add the openid scope (mandatory)
  • Add nonce on the request, validate it on the ID token
  • Token response now includes id_token alongside access_token
  • You can also call /userinfo with the access token

Three new roles' names

  • Relying Party (RP) β€” your app (= OAuth Client)
  • OpenID Provider (OP) β€” the identity service (= OAuth AS)
  • End-User β€” the human (= OAuth Resource Owner)
  • Same actors, different names β€” one of OIDC's needless terminology shifts

The hidden requirement

An OIDC-conformant flow must use Authorisation Code + PKCE + nonce. Implicit and hybrid response types still exist in the spec but are deprecated by FAPI and most modern profiles.

05

Core Flow β€” Authorisation Code with openid

User-Agent Relying Party OpenID Provider UserInfo (opt.) 1. click "Sign in with X" 2. 302 β†’ /authorize?scope=openid&nonce=…&PKCE 3. user authenticates & consents at OP 4. 302 redirect_uri?code=…&state=… 5. browser delivers ?code=… 6. POST /token (code, code_verifier, client_id+secret) 7. { access_token, refresh_token, id_token } 8. (optional) GET /userinfo Authorization: Bearer <access_token> 9. { sub, email, name, picture, … }

Steps 1–5: front-channel

Browser-visible. Carries the code only β€” no tokens.

Steps 6–7: back-channel

Server-to-server, TLS. ID token comes here. PKCE verifier required.

Steps 8–9: optional

UserInfo for fresh attributes; the ID token alone is enough for login.

06

The ID Token β€” Anatomy

An ID token is a signed JWT. Three dot-separated base64url parts: header, payload, signature. The OP signs; the RP verifies offline via JWKS.

Decoded header + payload

# Header
{
  "alg": "RS256",
  "kid": "abc123",
  "typ": "JWT"
}

# Payload β€” required claims
{
  "iss": "https://accounts.google.com",
  "sub": "1098765432109876543210",
  "aud": ".apps.googleusercontent.com",
  "iat": 1714000000,
  "exp": 1714003600,
  "nonce": "abc123"
}

Required claims (RFC 8259)

ClaimMeaning
issIssuer URL β€” must match OP's metadata
subStable user-ID at this OP
audYour client ID β€” exact match
iatIssued at (Unix seconds)
expExpiry β€” typically 5–15 min
nonceEchoes the value you sent

Common optional claims

  • auth_time β€” when the user actually authenticated
  • acr β€” authentication context class (e.g. urn:mace:incommon:iap:silver)
  • amr β€” authentication methods (["pwd","mfa","otp"])
  • azp β€” authorised party (when token has multiple aud)
  • at_hash β€” hash of access token, binds them together
  • c_hash β€” hash of code (hybrid flow)

Standard profile claims

  • email, email_verified
  • name, given_name, family_name, preferred_username
  • picture, locale, zoneinfo
  • phone_number, phone_number_verified
  • Released only if requested via profile, email, phone scopes β€” and only if the OP exposes them

Use sub as the user ID

Never use email. Email addresses can be transferred between people; sub is a stable opaque ID per OP. Composite key: (iss, sub).

07

ID Token Validation β€” The Checklist

Every step here has been the source of a real-world breach. Use a conformant library β€” jose, oidc-client-ts, openid-client, Microsoft.IdentityModel, Authlib. Don't write this yourself.

Cryptographic checks

  1. Parse header; refuse alg=none
  2. Refuse HS256 if you only have a public key (alg-confusion CVE-2015-9235 family)
  3. Look up key by kid in the OP's cached JWKS
  4. Verify the signature with that key
  5. If kid not found, refresh JWKS once and retry

Claim checks

  1. iss matches the OP discovery's issuer exactly (string-equal)
  2. aud contains your client_id (and only it)
  3. If aud is multi-value, azp equals your client_id
  4. exp is in the future (with small clock skew, ≀ 5 min)
  5. iat is not unreasonably in the future
  6. nonce equals the value you sent
  7. If you need step-up: check auth_time, acr, amr

JWKS rotation hygiene

  • Cache JWKS for minutes, not days
  • On unknown kid: refetch once and retry; otherwise reject
  • Pin the OP's TLS root if the JWKS endpoint can be MITM'd
  • OPs rotate keys regularly β€” Google rotates roughly daily

Library example (TypeScript / jose)

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://accounts.google.com/.well-known/jwks.json')
);

const { payload } = await jwtVerify(idToken, JWKS, {
  issuer:   'https://accounts.google.com',
  audience: process.env.GOOGLE_CLIENT_ID,
  clockTolerance: 5,
});

if (payload.nonce !== sessionNonce) throw new Error('nonce');
return { id: payload.sub, email: payload.email };

Don't use the access token for identity

Access tokens are not signed for you, may be opaque, and aren't bound to an aud you control. Always validate the ID token for "who is this user".

08

Discovery β€” /.well-known/openid-configuration

Discovery turns "where is the token endpoint?" from configuration into data. The RP fetches one JSON document from the OP at start-up; everything else falls out of it.

A real example

$ curl https://accounts.google.com/.well-known/openid-configuration

{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint":
      "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint":
      "https://oauth2.googleapis.com/token",
  "userinfo_endpoint":
      "https://openidconnect.googleapis.com/v1/userinfo",
  "revocation_endpoint":
      "https://oauth2.googleapis.com/revoke",
  "jwks_uri":
      "https://www.googleapis.com/oauth2/v3/certs",
  "response_types_supported":   ["code","token","id_token", ...],
  "subject_types_supported":    ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported":           ["openid","email","profile"],
  "claims_supported":           ["aud","email", ... ]
}

What to do with it

  • Cache for an hour or two (it almost never changes)
  • Use it to populate every endpoint URL β€” never hard-code them
  • Re-fetch on JWKS miss or after a long pause
  • Validate iss in tokens against this exact issuer string

Multi-tenant OPs

  • Microsoft Entra: https://login.microsoftonline.com/{tenant}/v2.0/.well-known/...
  • Azure B2C: {tenant}.b2clogin.com/{tenant}/{user-flow}/v2.0/.well-known/...
  • Auth0: https://{tenant}.auth0.com/.well-known/...
  • Discovery URL = identity = source of truth for the issuer string

The "issuer mismatch" trap

Common.com Live Microsoft tokens use a different issuer per tenant; if your code hard-codes "https://login.microsoftonline.com/common/v2.0" you will reject every real customer's token. Always resolve issuer from tid claim ↔ tenant-specific discovery.

09

JWKS β€” Public Keys & Rotation

A JWKS document

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "1f7d…",
      "n":   "vK… (modulus, base64url)",
      "e":   "AQAB"
    },
    {
      "kty": "EC",
      "use": "sig",
      "alg": "ES256",
      "kid": "5b9c…",
      "crv": "P-256",
      "x":   "…", "y": "…"
    }
  ]
}

Rotation lifecycle

  1. OP adds the new key to JWKS; signs nothing with it yet
  2. RPs that re-cache pick up the new kid
  3. OP starts signing with the new key
  4. Old key remains in JWKS until all in-flight tokens expire
  5. OP removes the old key

Cadence in the wild

  • Google ~ every 24 h
  • Microsoft Entra ~ every 24 h, more in incidents
  • Apple ~ every 6 months
  • Auth0 / Okta automatic, configurable
  • Keycloak manual or scheduled, default off β€” set a rotation policy

RP responsibilities

  • Cache JWKS at most an hour or two
  • On unknown kid: refetch once, retry; cache result
  • Don't fetch on every request β€” it's a DoS amplifier on the OP
  • Use a library with built-in caching (jose, jwks-rsa, python-jose, Microsoft.IdentityModel)

The 24-hour outage

If your JWKS cache TTL > OP rotation cadence, you will silently fail to validate new tokens once the OP rotates. Real-world: many small services that "worked fine" for months break at the next rotation.

10

Scopes & UserInfo

OIDC standard scopes

ScopeReleases
openidRequired marker β€” without it, no ID token
profilename, family_name, given_name, picture, locale, …
emailemail, email_verified
addressPostal address claim object
phonephone_number, phone_number_verified
offline_accessIssue a refresh token alongside

UserInfo endpoint

GET /userinfo
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

200 OK
{
  "sub":            "1098765432109876543210",
  "email":          "alice@example.com",
  "email_verified": true,
  "name":           "Alice Example",
  "given_name":     "Alice",
  "family_name":    "Example",
  "picture":        "https://lh3...",
  "locale":         "en"
}

When to use UserInfo

  • Refreshing user attributes mid-session β€” name, picture changed
  • Keeping ID tokens small (release lots of claims via UserInfo, not id_token)
  • Backwards-compatibility with apps that expect a /userinfo

When not to

  • For login itself β€” the ID token is enough; UserInfo is one more network hop
  • If your OP returns UserInfo as a JWT (some do), you have to validate that signature too
  • Heavy traffic β€” cache UserInfo per session, not per request

Aggregated & distributed claims

For very large or sensitive attributes (medical, government), OIDC supports claims that point to a different endpoint with their own access tokens β€” rarely seen in practice, but the spec exists.

11

Response Types β€” Code, Hybrid, Implicit

OIDC inherits OAuth's response_type and adds three more. Most are now deprecated; one is the modern default.

response_typeReturns from /authorizeStatus (2025)
codecodeRecommended β€” Authorisation Code + PKCE
id_tokenid_token (no access token)Deprecated β€” implicit, no PKCE
id_token tokenid_token + access_tokenDeprecated β€” full implicit
code id_tokencode + id_token (hybrid)Niche β€” front-channel ID + back-channel access
code tokencode + access_token (hybrid)Avoid
code id_token tokenall threeAvoid

Why hybrid (code id_token) ever existed

  • Some apps wanted to verify the user identity before doing the back-channel token exchange
  • Useful for FAPI 1.0 (Open Banking pre-2022); largely superseded by JARM / pushed authorisation requests

The 2025 default

response_type=code + PKCE + nonce + Authorisation Code Flow. Same as OAuth 2.1's only-allowed flow. Everything else is a legacy compatibility lever.

12

Logout & Session Management

Login is the easy part. Logging the user out everywhere is the hard one β€” OIDC has three mechanisms, none of them perfect.

RP-Initiated Logout

  • RP redirects user to the OP's end_session_endpoint
  • Carries id_token_hint + post_logout_redirect_uri
  • OP terminates its session, redirects back
  • Simple but β€” if the user has 5 RPs open, only this one signs out

Back-Channel Logout

  • OP POSTs a signed Logout Token to every RP that is registered for it
  • RP invalidates the user's session in its own store
  • Works even if the user closes the browser
  • Robust; common in Keycloak / Okta / Auth0

Front-Channel Logout

  • OP renders an iframe per RP pointing at frontchannel_logout_uri
  • Each iframe signs the user out client-side
  • Browser-dependent; blocked by third-party cookie restrictions
  • Largely superseded by back-channel

Single-logout reality

Truly synchronous SLO across N apps is hard. The pragmatic stance: back-channel + short ID-token TTLs + refresh-token revocation. Accept eventual consistency for tabs already open.

The 3rd-party-cookie cliff

Front-channel logout, the OP's check_session_iframe, and many SSO mechanisms relied on third-party cookies. Chrome's deprecation timeline (2024–2026) is forcing migration to back-channel logout and FedCM-style browser APIs.

13

CIBA β€” Client-Initiated Backchannel Authentication

The user is at one device (a kiosk, a call-centre agent's screen) and approves on another (their phone). No browser redirect. FAPI's flow of choice for high-assurance approvals.

Sequence

  1. RP POSTs to /bc-authorize with a login_hint identifying the user
  2. OP returns an auth_req_id and a polling/notify mode
  3. OP pushes a notification to the user's phone
  4. User approves
  5. RP polls /token with the auth_req_id (or receives a webhook)
  6. OP returns ID token + access token

Where it's used

  • UK Open Banking step-ups (FAPI-CIBA)
  • Mobile-confirm-on-laptop logins (some banks)
  • Call-centre identity verification flows
  • Smart-TV approval (a less-formal variant β€” OAuth Device Code grant, RFC 8628)

CIBA vs Device Code

  • Device Code (RFC 8628) β€” TVs, CLIs; user types a code on their phone
  • CIBA β€” RP knows the user's identity already and pings them
  • Both approve on a different device; the trigger is different

Implementations

  • Keycloak β€” full CIBA support
  • ForgeRock / Ping Identity β€” Open Banking deployments
  • Auth0 β€” limited preview
  • Microsoft Entra β€” no native CIBA; use Conditional Access & phone-sign-in instead
14

SIOPv2 & Verifiable Credentials

The 2020s rewrite of "decentralised identity". SIOPv2 (Self-Issued OpenID Provider v2) lets a wallet on the user's device act as its own OP. The RP gets an ID token signed by the wallet, plus optional Verifiable Credentials issued by trusted authorities.

The shift

  • Classic OIDC: OP is Google/Microsoft, the RP trusts them
  • SIOPv2: OP is the user's wallet (Apple Wallet, Google Wallet, EU Digital Identity Wallet), the RP trusts the credentials inside
  • Public-key cryptography on the device, not server-side accounts
  • Built on OAuth + OIDC + Verifiable Credentials Data Model (W3C)

Real deployments

  • EU Digital Identity Wallet β€” pilots since 2023, mandatory for member states by 2026
  • eIDAS 2.0 β€” legal framework, 2024
  • UK Digital Identity Trust Framework β€” voluntary, 2024+
  • US mDLs (mobile driving licences) β€” California, Arizona, Maryland

Sister specs

  • OID4VCI β€” OpenID for Verifiable Credential Issuance β€” how a wallet receives credentials from an issuer
  • OID4VP β€” OpenID for Verifiable Presentations β€” how a wallet shows credentials to a verifier (your app)
  • OpenID Federation 1.0 β€” how trust anchors are expressed (next slide)

For builders today

If you're building a generic web app, SIOPv2 is on the horizon, not the requirement. If you're integrating with EU public services from 2026, or healthcare / banking from a regulated authority, it will land on your roadmap. Watch Sphereon, walt.id, Mattr, Fido Alliance for SDKs.

15

OpenID Federation 1.0 (Sep 2024)

How do thousands of OPs and RPs trust each other without manual key exchange? OpenID Federation 1.0 β€” finalised Sep 2024 after a long draft phase β€” answers that with a tree of signed metadata.

The model

  • Every entity (RP, OP, intermediate, trust anchor) publishes an Entity Configuration at /.well-known/openid-federation
  • Each Entity Configuration is a signed JWT
  • It points up at one or more "superior" entities (its parents in the tree)
  • A "trust chain" from RP up to a Trust Anchor proves "this RP belongs to the federation"

Why it matters

  • Open Banking (Brazil, Australia)
  • EU eIDAS 2.0 trust framework
  • Research & Education federations (eduGAIN successor)
  • Any sector with N-to-N OP/RP relationships and a regulator

Vs the SAML federation predecessor

  • SAML 2.0 metadata (eduGAIN, InCommon) was XML-signed, fetched on schedule
  • OIDC Federation: JWT-based, dynamic resolution at runtime
  • Solves the "static-XML metadata gets stale" problem
  • First deployments expected through 2025–2026

For most teams

OpenID Federation is not yet a daily-driver concern. If your stakeholders are governments, financial regulators, or large research consortia, start tracking it; otherwise, the classic OIDC discovery + per-RP registration is still the standard.

16

FAPI 2.0 β€” Financial-Grade OIDC

FAPI (Financial-grade API) is an OpenID Foundation security profile. It pins down which OAuth/OIDC parameters and algorithms are mandatory for high-value APIs. FAPI 2.0 Security Profile reached Implementer's Draft 2 in 2024 and is the basis for Open Banking 2.0+.

FAPI 2.0 mandates

  • PAR β€” Pushed Authorisation Requests (RFC 9126)
  • PKCE β€” always
  • Sender-constrained tokens β€” DPoP (RFC 9449) or mTLS (RFC 8705)
  • private_key_jwt or mTLS for client auth
  • Strong algorithms only β€” RS256/PS256/ES256; no HS-anything
  • Authorisation Code only; nothing implicit

Where you'll meet it

  • UK / EU / Brazil / Australia Open Banking
  • Saudi Arabia SAMA Open Banking
  • Insurance, pensions, capital markets (slowly)
  • Any new regulated API written this decade

Practical implications

  • Your OIDC stack must speak DPoP / mTLS β€” or you're not FAPI
  • Plain bearer tokens are not FAPI-compliant
  • Library landscape: most OSS libs need FAPI plugins (Authlete, ForgeRock, Connect2id are FAPI-certified)
  • Conformance testing is published by the OpenID Foundation

FAPI 2.0 vs OAuth 2.1

  • OAuth 2.1 is the baseline β€” what to do, what not to do
  • FAPI 2.0 is a profile β€” which of the optional extensions you must use
  • FAPI 2.0-compliant β‡’ OAuth 2.1-compliant; the reverse is not true

Don't pretend you're FAPI

FAPI conformance is testable. Customers in regulated sectors will run the OpenID Foundation conformance suite against your OP. Adding "FAPI-ready" to your marketing without certification is a fast way to lose a deal.

17

OIDC vs SAML β€” Which When

PropertySAML 2.0OIDC 1.0
Year20052014
EncodingXMLJSON / JWT
Native toBrowser onlyBrowser, mobile, M2M
Mobile / SPA supportPainfulFirst-class
Signature formatXML-DSig (notoriously fiddly)JWS (clean, well-tested libraries)
DiscoverySAML metadata XML/.well-known/openid-configuration
FederationeduGAIN, InCommon (mature)OpenID Federation 1.0 (just shipped, 2024)
LogoutSLO, SP-initiated, IdP-initiatedRP-initiated, back-channel, front-channel
Enterprise toolingDecades of vendor supportIncreasingly the default for new vendors
"It just works" librariesFew; high TCOMany; low TCO

Pick OIDC when…

  • You're building anything mobile, SPA, or native
  • You need M2M too (use Client Credentials)
  • You want fewer XML headaches
  • Your customers are tech-forward

Stay with SAML when…

  • You're selling B2B and Acme Corp's IT team only knows ADFS
  • You're integrating with eduGAIN / InCommon today
  • The IdP is on-prem and ancient (most enterprise IdPs < 2018 are SAML-only)

Practical answer for B2B SaaS: support both. WorkOS / Auth0 / Okta normalise SAML and OIDC behind one API.

18

Real-World OPs β€” The Quirks

OPIssuer URLQuirks worth knowing
Google https://accounts.google.com Issues email_verified; rotates JWKS daily; offline access requires access_type=offline&prompt=consent
Microsoft Entra https://login.microsoftonline.com/{tid}/v2.0 Tenant-scoped issuer; v1 vs v2 endpoints; tid claim identifies tenant; "common" endpoint exists but resolves per-token
Apple Sign in with Apple https://appleid.apple.com Returns email only on first login; relays via private email; email_verified is "true" but Apple-private
GitHub OAuth 2.0; no real OIDC; UserInfo-equivalent at /user Often retrofitted as OIDC by libs (e.g. NextAuth); not actually conformant
Okta / Auth0 https://{tenant}.okta.com / {tenant}.auth0.com Per-tenant; very FAPI/OIDC-conformant; some custom claim namespaces
Keycloak https://kc/realms/{realm} OSS; FAPI plugin; manual key rotation by default
ZITADEL per-tenant URL OSS; multi-tenant SaaS; full OIDC + Federation 1.0 roadmap
AWS Cognito https://cognito-idp.{region}.amazonaws.com/{poolId} Cheap; UX has rough edges; OIDC discovery only on user pools, not identity pools

Library landscape

  • jose (TS / Python / Go) β€” JWT primitives
  • openid-client (Node) β€” full RP toolkit by Filip Skokan
  • Authlib (Python) β€” OIDC/OAuth client & server
  • Microsoft.IdentityModel (.NET) β€” first-party
  • Pac4j (Java) β€” OIDC + SAML
  • NextAuth.js / Auth.js β€” SSR framework integrations

Companion deck

The full Auth-as-a-Service provider tour, with self-hosted options and the OAuth specification trail, is in the Introduction to OAuth and OAuth for MCP decks.

19

Workload OIDC β€” The Modern CI/CD Pattern

Beyond user login, OIDC has quietly become the way workloads authenticate. GitHub Actions, GitLab CI, Buildkite, CircleCI, K8s clusters all sign OIDC tokens; the cloud trusts those tokens via configured issuer URLs. No long-lived static keys.

GitHub Actions β†’ AWS

# GitHub action workflow:
permissions:
  id-token: write           # request OIDC token

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123:role/deploy
          aws-region: eu-west-2
      # β†’ role assumed via STS using OIDC; creds last 1h
      - run: aws s3 cp ./dist s3://my-bucket --recursive

AWS-side trust policy

{
  "Effect":"Allow",
  "Principal":{"Federated":
    "arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com"},
  "Action":"sts:AssumeRoleWithWebIdentity",
  "Condition":{
    "StringEquals":{
      "token.actions.githubusercontent.com:aud":"sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub":
         "repo:acme/api:ref:refs/heads/main"
    }
  }
}

The pattern, generalised

  1. CI runner has a private key per job
  2. Runner signs an OIDC ID token containing claims about the job (repo, ref, environment)
  3. Cloud STS trusts the runner's JWKS endpoint as an OIDC issuer
  4. Cloud STS verifies the JWT and the per-claim conditions
  5. Cloud STS issues short-lived workload credentials (15m–12h)

Same pattern, other providers

  • GCP Workload Identity Federation
  • Azure Workload Identity Federation
  • AWS IAM Roles Anywhere β€” for non-CI external workloads with TLS certs
  • EKS Pod Identity / IRSA / GKE Workload Identity / AKS Workload Identity β€” same idea inside K8s clusters
  • SPIFFE / SPIRE β€” vendor-neutral workload identity, OIDC-style trust on top

Trust the right claims

Trust the issuer alone gives access to every workload at GitHub. Always condition on repo:, ref:, and ideally environment:. Misconfigurations here have given external repos access to production AWS accounts.

20

Common Bugs & Mitigations

Trusting email as identity

Two users could have the same verified email at different OPs. Always key on (iss, sub); treat email as a hint.

Hard-coded JWKS URL

Provider rotates infra; JWKS URL changes. Resolve via discovery, every time, with a sane cache TTL.

Skipping nonce validation

Replay attack: an attacker reuses a captured ID token for a different login attempt. Generate a fresh nonce per request, store in session, compare on return.

Trusting email_verified=false

Some OPs return unverified email addresses. Reject them, or treat them as "anonymous, please verify in-app".

PII in ID tokens, then shipped to logs

ID tokens often contain email and name. Log only the JTI / sub, never the raw token; strip claims from telemetry.

alg confusion

If your library accepts whatever alg the JWT header asks for, an attacker can swap RS256 for HS256 and sign with the public key. Pin allowed algs.

Multi-tenant issuer trust

Trusting https://login.microsoftonline.com/common/v2.0 trusts every Microsoft tenant. Always resolve to the specific tenant's issuer using the tid claim, then validate.

Long-lived ID tokens

ID tokens should be 5–15 minutes. Re-derive sessions from the access/refresh-token pair, not from the original ID token, after the first hop.

21

Summary

Three takeaways

  1. OIDC = OAuth 2.0 + the openid scope + an ID token. Same flow you already know; one extra signed assertion comes back.
  2. The complexity is in validation: signatures, issuer, audience, nonce, JWKS rotation. Use a conformant library and don't roll your own.
  3. OIDC has quietly outgrown "Sign in with Google" β€” workload OIDC, FAPI 2.0, OpenID Federation 1.0 and SIOPv2 are reshaping identity for the next decade.

When you'll meet OIDC

  • "Sign in with X" buttons
  • B2B SaaS SSO (decks 04 of the *aaS series)
  • CI/CD federation (deck 05 of the *aaS series)
  • Open Banking, eIDAS, regulated APIs
  • Verifiable credentials & digital wallets β€” the next wave

Companion decks

One sentence

"OAuth answers 'may this app act for the user'; OIDC answers 'who is the user' β€” and almost every login button you've clicked in the last decade is the second of those, riding on the first."

References

OpenID Connect Core 1.0 Β· Discovery 1.0 Β· Dynamic Client Registration 1.0 Β· Session Management Β· RP-Initiated Logout Β· Back-Channel Logout Β· CIBA Core Β· SIOPv2 Β· OID4VCI / OID4VP Β· OpenID Federation 1.0 Β· FAPI 2.0 Security Profile Β· RFC 7519 (JWT) Β· RFC 7515/16/17/18 (JWS/JWE/JWA/JWK) Β· RFC 9068 (JWT Profile for Access Tokens) Β· RFC 9126 (PAR) Β· RFC 9449 (DPoP) Β· openid.net