{"id":960,"date":"2025-09-10T08:50:25","date_gmt":"2025-09-10T08:50:25","guid":{"rendered":"https:\/\/www.rajeshkumar.xyz\/blog\/?p=960"},"modified":"2025-09-10T08:51:23","modified_gmt":"2025-09-10T08:51:23","slug":"jwt-oidc-the-plain-english-guide-from-fundamentals-to-advanced","status":"publish","type":"post","link":"https:\/\/www.rajeshkumar.xyz\/blog\/jwt-oidc-the-plain-english-guide-from-fundamentals-to-advanced\/","title":{"rendered":"JWT &amp; OIDC \u2014 The Plain-English Guide (from Fundamentals to Advanced)"},"content":{"rendered":"\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/sdmntprsouthcentralus.oaiusercontent.com\/files\/00000000-69e8-61f7-b5aa-dff072d87e64\/raw?se=2025-09-10T09%3A50%3A52Z&amp;sp=r&amp;sv=2024-08-04&amp;sr=b&amp;scid=540e35d4-1cba-504e-a8d9-526e2a69db6f&amp;skoid=732f244e-db13-47c3-bcc7-7ee02a9397bc&amp;sktid=a48cca56-e6da-484e-a814-9c849652bcb3&amp;skt=2025-09-09T19%3A04%3A12Z&amp;ske=2025-09-10T19%3A04%3A12Z&amp;sks=b&amp;skv=2024-08-04&amp;sig=0uJZT7tWtjPXqDc%2BPINJ2do\/TYl2t7JXRY9yySTu66M%3D\" alt=\"\"\/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83e\udde9 JWT vs OIDC \u2014 Separate but Together<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">1. <strong>JWT (JSON Web Token)<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>What it is<\/strong>: A token format (just a container for claims).<\/li>\n\n\n\n<li><strong>What it looks like<\/strong>: <code>header.payload.signature<\/code><\/li>\n\n\n\n<li><strong>Who uses it<\/strong>: Both humans <em>and<\/em> services.<\/li>\n\n\n\n<li><strong>What it carries<\/strong>: Facts like:\n<ul class=\"wp-block-list\">\n<li><code>sub<\/code> \u2192 who the token is about<\/li>\n\n\n\n<li><code>aud<\/code> \u2192 who the token is for<\/li>\n\n\n\n<li><code>exp<\/code> \u2192 when it expires<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>\ud83d\udc49 JWT is like a <strong>passport card<\/strong> \u2014 it holds information and a signature that proves authenticity.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. <strong>OIDC (OpenID Connect)<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>What it is<\/strong>: A protocol (set of rules) built on top of OAuth2.<\/li>\n\n\n\n<li><strong>Purpose<\/strong>: To let apps log users in and know <em>who they are<\/em>.<\/li>\n\n\n\n<li><strong>What it gives you<\/strong>:\n<ul class=\"wp-block-list\">\n<li><strong>ID Token<\/strong> \u2192 usually a <strong>JWT<\/strong> that says \u201cThis user is Alice, verified by Google\/Okta\/etc.\u201d<\/li>\n\n\n\n<li><strong>Access Token<\/strong> \u2192 used to call APIs (sometimes also a JWT).<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>\ud83d\udc49 OIDC is like the <strong>border control process<\/strong> \u2014 the steps (passport check, stamps) that result in you getting that <strong>passport card (JWT)<\/strong> to travel with.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. <strong>How they fit together<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>JWT = the format<\/strong><\/li>\n\n\n\n<li><strong>OIDC = the login protocol<\/strong> that issues JWTs<\/li>\n<\/ul>\n\n\n\n<p>So:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A user logs in via <strong>OIDC<\/strong>.<\/li>\n\n\n\n<li>The IdP (e.g., Google, Okta, Cognito) hands back an <strong>ID Token<\/strong>, which is a <strong>JWT<\/strong>.<\/li>\n\n\n\n<li>Your app or API validates the JWT.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. <strong>Use cases side by side<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Situation<\/th><th>What you use<\/th><th>Example<\/th><\/tr><\/thead><tbody><tr><td>Human login<\/td><td><strong>OIDC \u2192 issues JWT (ID Token)<\/strong><\/td><td>User signs in with Google \u2192 app gets ID Token JWT<\/td><\/tr><tr><td>Service calling service<\/td><td><strong>JWT directly (OAuth2 Client Credentials)<\/strong><\/td><td>Microservice A calls Microservice B using JWT access token<\/td><\/tr><tr><td>API Gateway<\/td><td>Validates <strong>JWTs<\/strong> (from OIDC or service flows)<\/td><td>AWS API Gateway checks JWT from Cognito or Firebase<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. \ud83d\udd11 Key takeaway<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>JWT is a token format.<\/strong><\/li>\n\n\n\n<li><strong>OIDC is a login protocol.<\/strong><\/li>\n\n\n\n<li><strong>OIDC tokens are JWTs.<\/strong><\/li>\n\n\n\n<li>For <strong>users<\/strong> \u2192 OIDC (login flow) \u2192 JWTs.<\/li>\n\n\n\n<li>For <strong>services<\/strong> \u2192 JWTs (via OAuth2 or workload identity).<\/li>\n<\/ul>\n\n\n\n<p>So yes \u2014 <strong>separate<\/strong> (format vs protocol), but always <strong>together<\/strong> in practice.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n\n\n\n<h1 class=\"wp-block-heading\">JWT &amp; OIDC \u2014 The Plain-English Guide (from Fundamentals to Advanced)<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">0) The 1-minute mental model<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>JWT<\/strong> = a <strong>sealed card<\/strong> (digitally signed) that carries facts (\u201cwho\/what\/when\/permissions\u201d). Anyone with the issuer\u2019s <strong>public key<\/strong> can check it hasn\u2019t been altered.<\/li>\n\n\n\n<li><strong>OIDC<\/strong> (OpenID Connect) = a <strong>login recipe for humans<\/strong> built on OAuth2. It tells apps <strong>how to log people in<\/strong> (redirects, consent) and returns an <strong>ID Token (a JWT)<\/strong> that says <em>who the user is<\/em>.<\/li>\n\n\n\n<li><strong>Access Token<\/strong> (often a JWT) = a <strong>permit<\/strong> you show to an API to prove what you\u2019re allowed to do.<\/li>\n\n\n\n<li><strong>Refresh Token<\/strong> = a <strong>backstage pass<\/strong> the app uses to quietly get new access tokens without asking the user to log in again.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1) What is JWT?<\/h2>\n\n\n\n<p>A compact, URL-safe <strong>token format<\/strong>: <code>header.payload.signature<\/code>.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Header<\/strong>: the signing algorithm (e.g., RS256).<\/li>\n\n\n\n<li><strong>Payload<\/strong>: claims like <code>sub<\/code> (subject\/user), <code>aud<\/code> (audience), <code>exp<\/code> (expiry).<\/li>\n\n\n\n<li><strong>Signature<\/strong>: proves the token was issued by the trusted issuer and not modified.<\/li>\n<\/ul>\n\n\n\n<p><strong>Key property:<\/strong> self-contained. You can validate it <strong>offline<\/strong> using the issuer\u2019s public key (published via <strong>JWKS<\/strong> URL).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2) What is OIDC?<\/h2>\n\n\n\n<p>A standard way to <strong>log users in<\/strong> (web\/mobile) using OAuth2. It defines:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>ID Token (JWT)<\/strong> \u2192 proves <em>who the user is<\/em> (identity).<\/li>\n\n\n\n<li><strong>UserInfo endpoint<\/strong> \u2192 optional place to fetch profile data.<\/li>\n\n\n\n<li><strong>Discovery document<\/strong> \u2192 where the app finds all endpoints &amp; keys.<\/li>\n<\/ul>\n\n\n\n<p><strong>OIDC \u2248 login flows; JWT \u2248 the token format OIDC uses for identity.<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3) Why do we need them?<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Single Sign-On<\/strong> across apps, teams and even companies.<\/li>\n\n\n\n<li><strong>Zero shared secrets<\/strong> between every service\u2014verify tokens with public keys.<\/li>\n\n\n\n<li><strong>Stateless performance<\/strong>\u2014no per-request DB lookup needed to validate tokens.<\/li>\n\n\n\n<li><strong>Portable and interoperable<\/strong>\u2014works across clouds and languages.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4) Common use-cases<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Human users (OIDC)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u201cLogin with Google\/Okta\/Azure AD\/Keycloak\/Cognito.\u201d<\/li>\n\n\n\n<li>Multi-app SSO (one login, many apps).<\/li>\n\n\n\n<li>MFA, passwordless, social login.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Service \u2194 Service (JWT)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>One microservice calling another with a <strong>short-lived JWT<\/strong> (often from OAuth2 <strong>Client Credentials<\/strong>).<\/li>\n\n\n\n<li>Jobs, webhooks, backends calling APIs without a human user.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Mixed<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>User logs in (OIDC) \u2192 frontend calls your API with an <strong>access token (JWT)<\/strong>.<\/li>\n\n\n\n<li>Backend calls downstream services using its <strong>own service JWT<\/strong>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5) How they work (step by step)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">A) OIDC \u201cAuthorization Code + PKCE\u201d (recommended for web\/SPAs\/mobile)<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>User opens your app<\/strong> \u2192 \u201cSign in\u201d.<\/li>\n\n\n\n<li>App <strong>redirects<\/strong> the user to the <strong>Identity Provider (IdP)<\/strong> (e.g., Google\/Okta\/Keycloak\/Cognito\/Firebase).<\/li>\n\n\n\n<li>User authenticates (password, MFA, passkey).<\/li>\n\n\n\n<li>IdP <strong>redirects back<\/strong> with a <strong>code<\/strong>.<\/li>\n\n\n\n<li>Your app (or backend) <strong>exchanges the code<\/strong> at the IdP\u2019s <strong>Token Endpoint<\/strong> \u2192 gets:\n<ul class=\"wp-block-list\">\n<li><strong>ID Token (JWT)<\/strong> = who the user is.<\/li>\n\n\n\n<li><strong>Access Token<\/strong> (often JWT) = what they can do.<\/li>\n\n\n\n<li><strong>Refresh Token<\/strong> (optional) for silent renewals.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>App calls your APIs with the <strong>access token<\/strong> in <code>Authorization: Bearer &lt;token><\/code>.<\/li>\n\n\n\n<li>APIs <strong>validate the token<\/strong> (issuer, audience, signature, expiry) and proceed.<\/li>\n<\/ol>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>PKCE<\/strong>: protects SPAs\/mobile apps by proving the app that started the login is the app finishing it.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">B) Service-to-Service (OAuth2 Client Credentials)<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Service A<\/strong> asks the IdP\/Authorization Server for a token using its <strong>client id\/secret<\/strong> (or mTLS).<\/li>\n\n\n\n<li>IdP issues a <strong>short-lived access token (JWT)<\/strong> with claims like <code>aud<\/code>=ServiceB, scopes\/roles.<\/li>\n\n\n\n<li>Service A calls Service B with <code>Authorization: Bearer \u2026<\/code>.<\/li>\n\n\n\n<li>Service B validates: <strong>signature<\/strong>, <strong>issuer<\/strong>, <strong>audience<\/strong>, <strong>exp<\/strong> (and optionally <strong>scope\/role<\/strong> claims).<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">C) Token validation (what every API should check)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Signature<\/strong>: verify with the issuer\u2019s <strong>JWKS<\/strong> public key.<\/li>\n\n\n\n<li><strong>Issuer (<code>iss<\/code>)<\/strong>: matches the IdP you trust.<\/li>\n\n\n\n<li><strong>Audience (<code>aud<\/code>)<\/strong>: matches <strong>your API<\/strong>.<\/li>\n\n\n\n<li><strong>Expiry (<code>exp<\/code>)<\/strong> and <strong>not before (<code>nbf<\/code>)<\/strong>.<\/li>\n\n\n\n<li><strong>Subject (<code>sub<\/code>)<\/strong> present; optionally <strong>scope\/roles<\/strong> to authorize actions.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6) Advanced (without the jargon)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Scopes vs Roles vs Claims<\/strong>\n<ul class=\"wp-block-list\">\n<li><em>Scopes<\/em> = what actions are allowed (e.g., <code>read:orders<\/code>).<\/li>\n\n\n\n<li><em>Roles<\/em> = grouped permissions (e.g., <code>admin<\/code>, <code>editor<\/code>).<\/li>\n\n\n\n<li><em>Claims<\/em> = facts inside the token (email, org, roles, tenant).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Key types<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>HS256<\/strong> (shared secret) \u2192 simple but you must protect the secret on <strong>every<\/strong> verifier.<\/li>\n\n\n\n<li><strong>RS256\/ES256<\/strong> (public\/private keys) \u2192 issuer keeps <strong>private key<\/strong>, everyone else only needs <strong>public key<\/strong> (safer &amp; common).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Key rotation<\/strong>\n<ul class=\"wp-block-list\">\n<li>IdP exposes <strong>JWKS<\/strong>. Your gateways\/servers should <strong>cache<\/strong> keys and <strong>auto-refresh<\/strong> (handle <code>kid<\/code> changes seamlessly).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Refresh tokens<\/strong>\n<ul class=\"wp-block-list\">\n<li>Keep them <strong>server-side<\/strong> or in <strong>HTTP-only<\/strong> secure cookies (not localStorage).<\/li>\n\n\n\n<li>Rotate on use; revoke on breach.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Opaque vs JWT access tokens<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>Opaque<\/strong>: must call the issuer\u2019s <strong>introspection endpoint<\/strong> each time.<\/li>\n\n\n\n<li><strong>JWT<\/strong>: validate offline; faster, but revocation is trickier.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Proof-of-Possession (DPoP\/mTLS)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Binds the token to a specific client key or TLS certificate \u2192 blocks token theft\/replay.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Token exchange \/ impersonation<\/strong>\n<ul class=\"wp-block-list\">\n<li>Trade one token for another (e.g., user token \u2192 service token limited to one API).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Logout<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>Front-channel<\/strong> (browser) or <strong>back-channel<\/strong> (server-to-server) logout, plus session cookies invalidation and refresh token revocation.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">7) Limitations &amp; gotchas (and how to avoid them)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Revocation is hard with JWT<\/strong><br>Use <strong>short lifetimes<\/strong> (e.g., 5\u201315 min) + <strong>refresh tokens<\/strong>. Revoke refresh tokens on logout\/compromise.<\/li>\n\n\n\n<li><strong>Too many claims = big tokens<\/strong><br>Keep tokens lean; fetch extra data from your user\/profile API when needed.<\/li>\n\n\n\n<li><strong>Audience\/issuer mismatches<\/strong><br>Many 401s boil down to wrong <code>aud<\/code> or <code>iss<\/code>. Double-check both.<\/li>\n\n\n\n<li><strong>Clock skew<\/strong><br>Allow 1\u20132 minutes skew on <code>iat\/nbf\/exp<\/code>.<\/li>\n\n\n\n<li><strong>Storing tokens in the browser<\/strong><br>Prefer <strong>HTTP-only secure cookies<\/strong> over localStorage (defense against XSS).<\/li>\n\n\n\n<li><strong>\u201calg: none\u201d \/ weak algorithms<\/strong><br>Only accept expected algorithms (e.g., RS256). Never allow <code>none<\/code>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">8) Where they fit in AWS, Google Cloud, Azure<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">AWS (Amazon)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Issuers \/ IdPs<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>Amazon Cognito User Pools<\/strong> (OIDC &amp; OAuth2; issues ID\/Access\/Refresh tokens as JWTs).<\/li>\n\n\n\n<li><strong>IAM OIDC\/Federation<\/strong> (workload identity, GitHub Actions, IRSA for EKS).<\/li>\n\n\n\n<li>External IdPs: <strong>Okta, Auth0, Keycloak, Google<\/strong> (federated to Cognito or directly to ALB\/API GW).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Validators \/ Enforcement<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>API Gateway (HTTP API)<\/strong> \u2192 <strong>JWT Authorizer<\/strong> (validates issuer\/jwks\/audience).<\/li>\n\n\n\n<li><strong>Application Load Balancer<\/strong> \u2192 <strong>authenticate-oidc<\/strong> action (runs the OIDC login flow) with <strong>Cognito\/Okta\/Keycloak\/Google<\/strong> (note: not Firebase as an OIDC provider for ALB).<\/li>\n\n\n\n<li><strong>EKS<\/strong>: NGINX\/Kong\/Envoy\/Istio gateways can validate JWTs; <strong>IRSA<\/strong> uses OIDC to map pods to IAM roles.<\/li>\n\n\n\n<li><strong>CloudFront<\/strong>: integrates with Cognito\/custom auth at the edge (via Lambda@Edge).<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Google Cloud<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Issuers \/ IdPs<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>Identity Platform \/ Firebase Auth<\/strong> (OIDC\/OAuth2; issues ID tokens as JWTs).<\/li>\n\n\n\n<li><strong>Google Workspace\/Cloud Identity<\/strong> for enterprise SSO.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Validators \/ Enforcement<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>API Gateway<\/strong> \/ <strong>Cloud Endpoints<\/strong> \u2192 JWT\/OIDC validation in the gateway config.<\/li>\n\n\n\n<li><strong>Cloud Run<\/strong>: private services trust <strong>Google-signed ID tokens<\/strong> (IAM); put Gateway in front to validate external JWT\/OIDC.<\/li>\n\n\n\n<li><strong>IAP (Identity-Aware Proxy)<\/strong> for web access control.<\/li>\n\n\n\n<li><strong>Load balancers<\/strong> + <strong>Cloud Armor<\/strong> (policy\/WAF) in front of Gateways\/Backends.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Microsoft Azure<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Issuers \/ IdPs<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>Microsoft Entra ID (formerly Azure AD)<\/strong> and <strong>Entra ID B2C<\/strong> (OIDC\/OAuth2).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Validators \/ Enforcement<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>API Management (APIM)<\/strong> \u2192 validate JWT (issuer\/jwks\/audience), add policies.<\/li>\n\n\n\n<li><strong>App Service Authentication (\u201cEasyAuth\u201d)<\/strong> \u2192 offload OIDC login to Entra\/others.<\/li>\n\n\n\n<li><strong>AKS<\/strong>: NGINX\/Kong\/Envoy\/Istio for JWT validation; <strong>Workload Identity<\/strong> uses OIDC to map pods to Azure managed identities.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">9) Quick recipes you can reuse<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\u2705 Human login (web\/app) \u2014 safest baseline<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>OIDC Authorization Code + PKCE<\/strong><\/li>\n\n\n\n<li>IDP: Cognito \/ Entra ID \/ Identity Platform (Firebase Auth) \/ Okta \/ Auth0 \/ Keycloak<\/li>\n\n\n\n<li><strong>Store<\/strong> access\/refresh tokens in <strong>HTTP-only secure cookies<\/strong><\/li>\n\n\n\n<li><strong>Rotate<\/strong> refresh tokens; short access token TTL (5\u201315 min)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\u2705 Service \u2192 Service<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>OAuth2 Client Credentials<\/strong> (mTLS if possible)<\/li>\n\n\n\n<li>Short TTL (5\u201310 min), audience-scoped to the target service<\/li>\n\n\n\n<li>Validate on the API: signature, <code>iss<\/code>, <code>aud<\/code>, <code>exp<\/code>, scopes\/roles<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\u2705 Ingress\/Gateway enforcement<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>AWS: <strong>API Gateway JWT Authorizer<\/strong> or <strong>ALB OIDC<\/strong> (with Cognito\/Okta\/etc.)<\/li>\n\n\n\n<li>GCP: <strong>API Gateway<\/strong> (JWT\/OIDC) \u2192 Cloud Run (private)<\/li>\n\n\n\n<li>Azure: <strong>APIM<\/strong> JWT policy or <strong>EasyAuth<\/strong><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">10) Security checklist (copy\/paste)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Access tokens expire \u2264 15 minutes; refresh tokens rotate.<\/li>\n\n\n\n<li>Validate <strong>signature, iss, aud, exp\/nbf<\/strong> on every API call.<\/li>\n\n\n\n<li>Accept <strong>only<\/strong> the algorithms you expect (e.g., RS256).<\/li>\n\n\n\n<li>Enforce <strong>least privilege<\/strong> with scopes\/roles.<\/li>\n\n\n\n<li>Use <strong>PKCE<\/strong> for SPAs\/mobile.<\/li>\n\n\n\n<li>Use <strong>HTTP-only secure cookies<\/strong> for tokens on the web.<\/li>\n\n\n\n<li>Implement <strong>logout<\/strong> (revoke refresh tokens, clear cookies, end IdP session).<\/li>\n\n\n\n<li>Monitor JWKS fetch\/rotation; handle <strong><code>kid<\/code><\/strong> changes.<\/li>\n\n\n\n<li>Add <strong>WAF\/rate limits<\/strong> at your gateway\/load balancer.<\/li>\n\n\n\n<li>Log <strong>token ID (<code>jti<\/code>)<\/strong>, <code>sub<\/code>, <code>aud<\/code> (no sensitive data) for audits.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">11) FAQ (super short)<\/h2>\n\n\n\n<p><strong>Is JWT only for machines and OIDC only for humans?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>JWT is a <strong>format<\/strong> used by both. OIDC is a <strong>login protocol<\/strong> for humans that <strong>uses JWT<\/strong> (ID Token).<\/li>\n<\/ul>\n\n\n\n<p><strong>Do I need OIDC for service-to-service?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Usually <strong>no<\/strong>. Use OAuth2 <strong>Client Credentials<\/strong> (returns a JWT access token).<\/li>\n<\/ul>\n\n\n\n<p><strong>Can I revoke a JWT immediately?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Not cleanly. Use <strong>short lifetimes<\/strong> + <strong>refresh token revocation<\/strong>. For high-risk actions, re-check with the IdP or use introspection.<\/li>\n<\/ul>\n\n\n\n<p><strong>What\u2019s the difference between an ID Token and an Access Token?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>ID Token<\/strong>: identity of the user (for your client).<\/li>\n\n\n\n<li><strong>Access Token<\/strong>: permission for an API (what can be done).<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">12) Your migration hint (EKS front door options)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Mirror GCP approach<\/strong>: <strong>AWS API Gateway (HTTP API) JWT Authorizer<\/strong> validating Firebase\/Auth0\/Okta\/Keycloak \u2192 VPC Link \u2192 EKS.<\/li>\n\n\n\n<li><strong>All inside EKS<\/strong>: ALB Ingress \u2192 in-cluster gateway (NGINX\/Kong\/Envoy) <strong>validates JWT<\/strong>.<\/li>\n\n\n\n<li><strong>ALB runs login<\/strong>: use <strong>Cognito\/Okta\/Keycloak\/Google<\/strong> for <strong>ALB OIDC<\/strong> (Firebase doesn\u2019t act as ALB\u2019s OIDC IdP).<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>let\u2019s expand that simple table into <strong>detailed workflows<\/strong> so you can see exactly what happens at each hop, for <strong>Human login (OIDC)<\/strong>, <strong>Service-to-Service (JWT)<\/strong>, and <strong>API Gateway validating JWTs<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83d\udd04 Detailed Workflows<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. \ud83d\udc64 Human Login (OIDC \u2192 issues JWT ID Token)<\/h2>\n\n\n\n<p><strong>Actors<\/strong>: User, Application (frontend + backend), Identity Provider (Google, Okta, Cognito, Firebase, etc.), API<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step-by-step<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>User opens app<\/strong>\n<ul class=\"wp-block-list\">\n<li>User clicks &#8220;Sign in&#8221;.<\/li>\n\n\n\n<li>App redirects them to the IdP (OIDC provider).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>IdP Login Page<\/strong>\n<ul class=\"wp-block-list\">\n<li>User enters username\/password, MFA, or social login.<\/li>\n\n\n\n<li>IdP authenticates them.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Authorization Code issued<\/strong>\n<ul class=\"wp-block-list\">\n<li>After login, IdP redirects back to app with a <strong>code<\/strong> (short-lived, single use).<\/li>\n\n\n\n<li>This avoids exposing tokens in browser URL.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>App exchanges code for tokens<\/strong>\n<ul class=\"wp-block-list\">\n<li>App\/backend calls IdP <strong>Token Endpoint<\/strong> with:\n<ul class=\"wp-block-list\">\n<li>The code<\/li>\n\n\n\n<li>Client ID\/secret<\/li>\n\n\n\n<li>PKCE verifier (for extra security in mobile\/SPAs)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>IdP responds with:\n<ul class=\"wp-block-list\">\n<li><strong>ID Token (JWT)<\/strong> \u2192 \u201cUser is Alice\u201d<\/li>\n\n\n\n<li><strong>Access Token (JWT\/opaque)<\/strong> \u2192 \u201cAlice can call API X\u201d<\/li>\n\n\n\n<li><strong>Refresh Token<\/strong> (optional).<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>App calls API with Access Token<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>Authorization: Bearer &lt;access_token><\/code>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>API validates Access Token<\/strong>\n<ul class=\"wp-block-list\">\n<li>Check signature (using IdP\u2019s JWKS keys).<\/li>\n\n\n\n<li>Check claims (<code>iss<\/code>, <code>aud<\/code>, <code>exp<\/code>).<\/li>\n\n\n\n<li>Enforce scopes\/roles.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>API returns response<\/strong>\n<ul class=\"wp-block-list\">\n<li>If valid, API serves data \u2192 app \u2192 user.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<p>\u2705 <strong>Key JWT role here<\/strong>: OIDC issues an <strong>ID Token (JWT)<\/strong> for identity, and an <strong>Access Token<\/strong> (often JWT) for authorization.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. \ud83d\udd17 Service-to-Service (JWT directly, OAuth2 Client Credentials)<\/h2>\n\n\n\n<p><strong>Actors<\/strong>: Service A (caller), Service B (target API), Identity Provider \/ Authorization Server<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step-by-step<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Service A needs to call Service B<\/strong>\n<ul class=\"wp-block-list\">\n<li>Example: Payment service \u2192 Order service.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Service A requests token<\/strong>\n<ul class=\"wp-block-list\">\n<li>Calls IdP\u2019s Token Endpoint with:\n<ul class=\"wp-block-list\">\n<li><code>client_id<\/code>, <code>client_secret<\/code> (or mTLS certificate).<\/li>\n\n\n\n<li>Grant type: <code>client_credentials<\/code>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>IdP issues Access Token (JWT)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Payload includes:\n<ul class=\"wp-block-list\">\n<li><code>iss<\/code> (issuer = IdP)<\/li>\n\n\n\n<li><code>aud<\/code> (audience = Service B)<\/li>\n\n\n\n<li><code>sub<\/code> (subject = Service A)<\/li>\n\n\n\n<li><code>exp<\/code> (expiry, e.g., 5 mins)<\/li>\n\n\n\n<li><code>scope<\/code> (permissions like <code>orders:read<\/code>).<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Service A calls Service B<\/strong>\n<ul class=\"wp-block-list\">\n<li>HTTP request with <code>Authorization: Bearer &lt;access_token><\/code>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Service B validates token<\/strong>\n<ul class=\"wp-block-list\">\n<li>Retrieves IdP\u2019s JWKS keys.<\/li>\n\n\n\n<li>Verifies signature, expiry, audience = Service B.<\/li>\n\n\n\n<li>Enforces scopes (only allowed actions).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Service B responds<\/strong>\n<ul class=\"wp-block-list\">\n<li>Request succeeds if valid.<\/li>\n\n\n\n<li>Otherwise returns <code>401 Unauthorized<\/code>.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<p>\u2705 <strong>Key JWT role here<\/strong>: The JWT itself <em>is the service identity<\/em>. No human involved.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. \ud83c\udf10 API Gateway validates JWT (edge enforcement)<\/h2>\n\n\n\n<p><strong>Actors<\/strong>: Client (user or service), API Gateway (AWS API Gateway \/ GCP API Gateway \/ Azure APIM \/ NGINX\/Kong), Backend service<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step-by-step<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Client sends request<\/strong>\n<ul class=\"wp-block-list\">\n<li>Attaches <code>Authorization: Bearer &lt;token><\/code> (could be user OIDC token or service JWT).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Gateway receives request<\/strong>\n<ul class=\"wp-block-list\">\n<li>Gateway is configured with <strong>issuer<\/strong> and <strong>JWKS URI<\/strong>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Gateway validates JWT<\/strong>\n<ul class=\"wp-block-list\">\n<li>Check <strong>signature<\/strong> against IdP\u2019s public keys.<\/li>\n\n\n\n<li>Check <strong>issuer<\/strong> matches trusted IdP.<\/li>\n\n\n\n<li>Check <strong>audience<\/strong> claim matches the API.<\/li>\n\n\n\n<li>Check <strong>expiry<\/strong>.<\/li>\n\n\n\n<li>Optionally check <strong>scopes\/roles<\/strong>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>If valid<\/strong> \u2192 forward request to backend.\n<ul class=\"wp-block-list\">\n<li>Adds headers (like <code>X-User-Id<\/code>, <code>X-Scopes<\/code>) if configured.<\/li>\n\n\n\n<li>Backend trusts Gateway because it knows only validated requests get through.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>If invalid<\/strong> \u2192 reject at Gateway.\n<ul class=\"wp-block-list\">\n<li>Return <code>401 Unauthorized<\/code> (or <code>403 Forbidden<\/code>).<\/li>\n\n\n\n<li>Backend never sees bad traffic.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<p>\u2705 <strong>Key JWT role here<\/strong>: Gateway uses JWT to <strong>decide at the edge<\/strong> whether traffic should even hit the backend.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83d\udcca Connected View (How they all tie together)<\/h1>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">(1) Human Login (OIDC)\nUser -&gt; OIDC Provider -&gt; ID Token (JWT) + Access Token -&gt; API Gateway -&gt; API\n\n(2) Service-to-Service (JWT)\nService A -&gt; IdP (OAuth2 Client Credentials) -&gt; Access Token (JWT) -&gt; Service B\n\n(3) API Gateway (Validator)\nAny Client -&gt; API Gateway -&gt; JWT Validation -&gt; Backend Service\n<\/code><\/span><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83d\udd11 Takeaway<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>JWT = the envelope<\/strong> (claims + signature).<\/li>\n\n\n\n<li><strong>OIDC = the login flow<\/strong> (for humans) that issues JWTs.<\/li>\n\n\n\n<li><strong>Service-to-service<\/strong> uses JWTs directly (no OIDC flow needed).<\/li>\n\n\n\n<li><strong>Gateways<\/strong> act as <strong>bouncers<\/strong>, validating JWTs before APIs are touched.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83e\udde9 JWT vs OIDC \u2014 Separate but Together 1. JWT (JSON Web Token) \ud83d\udc49 JWT is like a passport card [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-960","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/posts\/960","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/comments?post=960"}],"version-history":[{"count":2,"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/posts\/960\/revisions"}],"predecessor-version":[{"id":963,"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/posts\/960\/revisions\/963"}],"wp:attachment":[{"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/media?parent=960"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/categories?post=960"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.rajeshkumar.xyz\/blog\/wp-json\/wp\/v2\/tags?post=960"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}