<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://blog.shaheerkj.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.shaheerkj.me/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-04-29T18:58:53+00:00</updated><id>https://blog.shaheerkj.me/feed.xml</id><title type="html">Shaheer Khalid</title><subtitle>Technical blog covering cloud security, identity, and DevOps by Shaheer Khalid</subtitle><entry><title type="html">OAuth 2.0: A Comprehensive Deep Dive</title><link href="https://blog.shaheerkj.me/posts/OAuth2.0-deep-dive/" rel="alternate" type="text/html" title="OAuth 2.0: A Comprehensive Deep Dive" /><published>2026-04-29T00:00:00+00:00</published><updated>2026-04-29T18:55:50+00:00</updated><id>https://blog.shaheerkj.me/posts/OAuth2.0-deep-dive</id><content type="html" xml:base="https://blog.shaheerkj.me/posts/OAuth2.0-deep-dive/"><![CDATA[<h2 id="oauth-20-a-comprehensive-technical-reference">OAuth 2.0: A Comprehensive Technical Reference</h2>

<blockquote>
  <p><strong>Audience:</strong> Security practitioners, cloud engineers, and developers studying IAM, SC-200, or modern authentication protocols.<br />
<strong>Goal:</strong> After reading this document, you should be able to explain OAuth 2.0 end-to-end, implement it securely, identify attack vectors, and make informed design decisions.</p>
</blockquote>

<hr />

<h3 id="table-of-contents">Table of Contents</h3>

<ol>
  <li><a href="#1-introduction">Introduction</a></li>
  <li><a href="#2-core-components">Core Components</a></li>
  <li><a href="#3-oauth-20-flows-grant-types">OAuth 2.0 Flows (Grant Types)</a></li>
  <li><a href="#4-protocol-internals">Protocol Internals</a></li>
  <li><a href="#5-end-to-end-example">End-to-End Example</a></li>
  <li><a href="#6-practical-exercises">Practical Exercises</a></li>
  <li><a href="#7-security-deep-dive">Security Deep Dive</a></li>
  <li><a href="#8-best-practices">Best Practices</a></li>
</ol>

<hr />

<h3 id="1-introduction">1. Introduction</h3>

<h4 id="11-what-is-oauth-20">1.1 What Is OAuth 2.0?</h4>

<p>OAuth 2.0 is an <strong>authorization framework</strong> defined in <a href="https://datatracker.ietf.org/doc/html/rfc6749">RFC 6749</a>. It enables a third-party application to obtain <strong>limited access</strong> to a service on behalf of a user — without ever seeing that user’s credentials.</p>

<p>The key word is <strong>authorization</strong> (what you’re allowed to do), not authentication (who you are). OAuth 2.0 does not, by itself, tell an application who the user is — that’s the job of <strong>OpenID Connect (OIDC)</strong>, which is a thin identity layer built on top of OAuth 2.0.</p>

<h4 id="12-why-does-oauth-20-exist">1.2 Why Does OAuth 2.0 Exist?</h4>

<h5 id="the-problem-credential-sharing">The Problem: Credential Sharing</h5>

<p>Before OAuth, if you wanted App A to access your data on Service B (e.g., a photo printing service accessing your Google Photos), the only option was to <strong>give App A your Google username and password</strong>.</p>

<p>This is catastrophically insecure:</p>

<ul>
  <li>App A now has full access to your Google account — not just photos</li>
  <li>You cannot revoke access without changing your password (which affects all apps)</li>
  <li>App A could store, leak, or misuse your credentials</li>
  <li>If App A is breached, your Google account is compromised</li>
</ul>

<h5 id="the-solution-delegated-authorization">The Solution: Delegated Authorization</h5>

<p>OAuth 2.0 replaces credential sharing with <strong>tokens</strong>. Instead of your password, App A receives a short-lived, scoped <strong>access token</strong> that only grants access to what you explicitly approved (e.g., read-only access to photos).</p>

<pre><code class="language-textile">WITHOUT OAuth:   User → gives password to App → App acts as User (unlimited access)
WITH OAuth:      User → approves limited scope → Auth Server issues token → App uses token (scoped access)
</code></pre>

<h4 id="13-authorization-vs-authentication">1.3 Authorization vs. Authentication</h4>

<p>This distinction is critical and frequently confused:</p>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>Question Answered</th>
      <th>Protocol</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Authentication</strong></td>
      <td>Who are you?</td>
      <td>OpenID Connect (OIDC), SAML</td>
    </tr>
    <tr>
      <td><strong>Authorization</strong></td>
      <td>What are you allowed to do?</td>
      <td>OAuth 2.0</td>
    </tr>
  </tbody>
</table>

<p>OAuth 2.0 answers: <em>“Is this token allowed to read the user’s calendar?”</em><br />
OIDC answers: <em>“Which user does this token belong to?”</em></p>

<blockquote>
  <p><strong>Important:</strong> OAuth 2.0 access tokens are <strong>not proof of identity</strong>. Never use the presence of a valid access token to conclude you know <em>who</em> the user is.</p>
</blockquote>

<h4 id="14-real-world-use-cases">1.4 Real-World Use Cases</h4>

<ul>
  <li><strong>“Login with Google” / “Login with GitHub”</strong> — Technically OpenID Connect (OAuth 2.0 + identity layer)</li>
  <li><strong>Spotify accessing your Facebook friends list</strong> — Classic OAuth 2.0 delegated authorization</li>
  <li><strong>A CI/CD pipeline pushing to GitHub</strong> — Client Credentials Flow (no user involved)</li>
  <li><strong>Smart TV apps</strong> — Device Code Flow (limited input capability)</li>
  <li><strong>Microsoft 365 integrations</strong> — Azure AD as the Authorization Server, various flows depending on client type</li>
</ul>

<hr />

<h3 id="2-core-components">2. Core Components</h3>

<h4 id="21-resource-owner">2.1 Resource Owner</h4>

<p>The <strong>Resource Owner</strong> is the entity that owns the data or resource being accessed — almost always a <strong>human user</strong>.</p>

<p>When you click “Allow” on a permission consent screen, you are acting as the Resource Owner, granting permission to a third-party application to access your data.</p>

<h4 id="22-client">2.2 Client</h4>

<p>The <strong>Client</strong> is the application requesting access to a protected resource on behalf of the Resource Owner. Clients come in two types based on their ability to keep secrets:</p>

<h5 id="confidential-clients">Confidential Clients</h5>

<p>Can securely store a <code class="language-plaintext highlighter-rouge">client_secret</code> (a password for the application itself). These run on servers the developer controls.</p>

<ul>
  <li><strong>Examples:</strong> Backend web servers, server-side APIs, daemon services</li>
  <li><strong>Can use:</strong> Authorization Code Flow, Client Credentials Flow</li>
  <li><strong>Key characteristic:</strong> The <code class="language-plaintext highlighter-rouge">client_secret</code> never leaves the server</li>
</ul>

<h5 id="public-clients">Public Clients</h5>

<p>Cannot securely store secrets because the code is exposed to the user’s device or environment.</p>

<ul>
  <li><strong>Examples:</strong> Single Page Applications (SPAs), mobile apps, desktop apps</li>
  <li><strong>Cannot use:</strong> Flows requiring <code class="language-plaintext highlighter-rouge">client_secret</code></li>
  <li><strong>Must use:</strong> PKCE (Proof Key for Code Exchange) to compensate</li>
  <li><strong>Key characteristic:</strong> Any secret embedded in the code can be extracted</li>
</ul>

<h4 id="23-authorization-server-as">2.3 Authorization Server (AS)</h4>

<p>The <strong>Authorization Server</strong> is the trusted party that:</p>

<ol>
  <li>Authenticates the Resource Owner (the user logs in here)</li>
  <li>Presents the consent screen</li>
  <li>Issues tokens (access tokens, refresh tokens, ID tokens)</li>
  <li>Validates token requests</li>
</ol>

<p>Examples: Google Identity Platform, Microsoft Entra ID (Azure AD), Okta, Auth0, Keycloak.</p>

<p>Key endpoints exposed by the AS:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GET /authorize</code> — starts the authorization flow</li>
  <li><code class="language-plaintext highlighter-rouge">POST /token</code> — exchanges codes/credentials for tokens</li>
  <li><code class="language-plaintext highlighter-rouge">GET /.well-known/openid-configuration</code> — discovery document</li>
</ul>

<h4 id="24-resource-server-rs">2.4 Resource Server (RS)</h4>

<p>The <strong>Resource Server</strong> hosts the protected resources (APIs, data). It:</p>

<ol>
  <li>Accepts requests containing access tokens (usually in the <code class="language-plaintext highlighter-rouge">Authorization: Bearer &lt;token&gt;</code> header)</li>
  <li>Validates the token (checks signature, expiry, scope)</li>
  <li>Returns the resource if the token is valid and has the right scope</li>
</ol>

<p>The Resource Server and Authorization Server are often operated by the same company but are logically separate components.</p>

<h4 id="25-tokens">2.5 Tokens</h4>

<h5 id="access-token">Access Token</h5>

<ul>
  <li>Short-lived credential that grants access to specific resources</li>
  <li>Typically expires in <strong>15 minutes to 1 hour</strong></li>
  <li>Sent with every API request: <code class="language-plaintext highlighter-rouge">Authorization: Bearer &lt;token&gt;</code></li>
  <li>Should be treated like a password — never log it, never expose it in URLs</li>
</ul>

<h5 id="refresh-token">Refresh Token</h5>

<ul>
  <li>Long-lived credential used to obtain new access tokens without re-authenticating</li>
  <li>Expires in <strong>days to weeks</strong> (or never, depending on configuration)</li>
  <li>Stored securely (httpOnly cookie or secure server-side storage)</li>
  <li>Can be <strong>revoked</strong> by the Authorization Server</li>
  <li>Only issued in flows where the user is present (not Client Credentials)</li>
</ul>

<h5 id="id-token-oidc">ID Token (OIDC)</h5>

<ul>
  <li>A <strong>JWT (JSON Web Token)</strong> containing claims about the authenticated user</li>
  <li>Only issued when the <code class="language-plaintext highlighter-rouge">openid</code> scope is requested</li>
  <li>Not intended to be sent to APIs — it’s for the <strong>client application</strong> to learn about the user</li>
  <li>Contains: <code class="language-plaintext highlighter-rouge">sub</code> (user ID), <code class="language-plaintext highlighter-rouge">email</code>, <code class="language-plaintext highlighter-rouge">name</code>, <code class="language-plaintext highlighter-rouge">iat</code> (issued at), <code class="language-plaintext highlighter-rouge">exp</code> (expiry), <code class="language-plaintext highlighter-rouge">iss</code> (issuer), <code class="language-plaintext highlighter-rouge">aud</code> (audience)</li>
</ul>

<hr />

<h3 id="3-oauth-20-flows-grant-types">3. OAuth 2.0 Flows (Grant Types)</h3>

<p>Different scenarios require different flows. The right flow depends on:</p>

<ul>
  <li>Who/what is the client? (user present? server-side? IoT device?)</li>
  <li>Can the client keep a secret?</li>
  <li>What level of trust is acceptable?</li>
</ul>

<hr />

<h4 id="31-authorization-code-flow">3.1 Authorization Code Flow</h4>

<p><strong>Best for:</strong> Confidential clients (server-side web apps) with a user present.</p>

<p>This is the <strong>most secure and widely used</strong> flow. It never exposes tokens in the browser URL bar.</p>

<h5 id="how-it-works-step-by-step">How It Works (Step by Step)</h5>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>```text
User          Browser/Client          Authorization Server          Resource Server
  |                  |                         |                          |
  |-- clicks login --&gt;|                         |                          |
  |                  |-- GET /authorize -------&gt;|                          |
  |                  |                         |                          |
  |&lt;--- redirected to AS login page -----------|                          |
  |-- enters credentials ---------------------&gt;|                          |
  |&lt;--- consent screen shown ------------------|                          |
  |-- approves -------------------------------&gt;|                          |
  |                  |&lt;-- redirect with code ---|                          |
  |                  |                         |                          |
  |                  |-- POST /token (code) ---&gt;|                          |
  |                  |&lt;-- access_token,        |                          |
  |                  |    refresh_token -------|                          |
  |                  |                         |                          |
  |                  |-- API request + Bearer token ----------------------&gt;|
  |                  |&lt;-- protected resource --------------------------------|
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#### Step 1: Authorization Request

The client redirects the user's browser to the AS:

```http
GET /authorize?
  response_type=code
  &amp;client_id=myapp-client-id
  &amp;redirect_uri=https://myapp.com/callback
  &amp;scope=read:profile read:email
  &amp;state=xK9mP2qR7vL4nJ1w
HTTP/1.1
Host: auth.example.com
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">response_type=code</code></td>
      <td>Tells AS to return an authorization code</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">client_id</code></td>
      <td>Identifies the application</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">redirect_uri</code></td>
      <td>Where to send the user after authorization</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scope</code></td>
      <td>What permissions are being requested</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">state</code></td>
      <td>Random value for CSRF protection — <strong>critical</strong></td>
    </tr>
  </tbody>
</table>

<h5 id="step-2-authorization-response">Step 2: Authorization Response</h5>

<p>After the user logs in and approves, the AS redirects back:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">302</span> <span class="ne">Found</span>
<span class="na">Location</span><span class="p">:</span> <span class="s">https://myapp.com/callback?</span>
<span class="s">  code=SplxlOBeZQQYbYS6WxSbIA</span>
<span class="s">  &amp;state=xK9mP2qR7vL4nJ1w</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">code</code> is short-lived (typically <strong>60 seconds</strong>) and single-use.</p>

<blockquote>
  <p>One thing I really thought about here was: <em>Why don’t we just return the access token here instead of auth code? Seemed redundant.</em> (AS) does <strong>not give the access token directly to the browser</strong> because the browser is a <strong>high-risk environment</strong>.</p>
</blockquote>

<h5 id="step-3-token-exchange">Step 3: Token Exchange</h5>

<p>The server-side client exchanges the code for tokens — this is a <strong>back-channel</strong> request (browser never sees it):</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Basic base64(client_id:client_secret)</span>

grant_type=authorization_code
&amp;code=SplxlOBeZQQYbYS6WxSbIA
&amp;redirect_uri=https://myapp.com/callback
</code></pre></div></div>

<h5 id="step-4-token-response">Step 4: Token Response</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiJ9..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8xLOxBtZp8"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"read:profile read:email"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This token is a <strong>Bearer token</strong>, and whoever <em>holds it</em> can use it to access the resource server</p>

<hr />

<h4 id="32-authorization-code-flow-with-pkce">3.2 Authorization Code Flow with PKCE</h4>

<p><strong>Best for:</strong> Public clients (SPAs, mobile apps) where a <code class="language-plaintext highlighter-rouge">client_secret</code> cannot be kept safe.</p>

<p>PKCE (Proof Key for Code Exchange, pronounced “pixie”) — defined in <a href="https://datatracker.ietf.org/doc/html/rfc7636">RFC 7636</a> — adds a cryptographic challenge to the Authorization Code Flow, preventing code interception attacks.</p>

<h5 id="how-pkce-works">How PKCE Works</h5>

<p>Before starting the flow, the client:</p>

<ol>
  <li>Generates a random <strong>code_verifier</strong> (43–128 character random string)</li>
  <li>Computes: <code class="language-plaintext highlighter-rouge">code_challenge = BASE64URL(SHA256(code_verifier))</code></li>
  <li>Sends <code class="language-plaintext highlighter-rouge">code_challenge</code> with the authorization request</li>
  <li>Sends <code class="language-plaintext highlighter-rouge">code_verifier</code> with the token request (proves it made the original request)</li>
</ol>

<p>Even if an attacker intercepts the authorization code, they cannot use it without the <code class="language-plaintext highlighter-rouge">code_verifier</code>, which was never sent over the network in plaintext.</p>

<h5 id="step-1-generate-pkce-values">Step 1: Generate PKCE Values</h5>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Generate a cryptographically random code_verifier</span>
<span class="kd">const</span> <span class="nx">code_verifier</span> <span class="o">=</span> <span class="nf">base64url</span><span class="p">(</span><span class="nx">crypto</span><span class="p">.</span><span class="nf">getRandomValues</span><span class="p">(</span><span class="k">new</span> <span class="nc">Uint8Array</span><span class="p">(</span><span class="mi">32</span><span class="p">)));</span>

<span class="c1">// Compute code_challenge</span>
<span class="kd">const</span> <span class="nx">code_challenge</span> <span class="o">=</span> <span class="nf">base64url</span><span class="p">(</span><span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nf">digest</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">SHA-256</span><span class="dl">'</span><span class="p">,</span>
  <span class="k">new</span> <span class="nc">TextEncoder</span><span class="p">().</span><span class="nf">encode</span><span class="p">(</span><span class="nx">code_verifier</span><span class="p">)</span>
<span class="p">));</span>
</code></pre></div></div>

<h5 id="step-2-authorization-request-with-pkce">Step 2: Authorization Request (with PKCE)</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?
  response_type=code
  &amp;client_id=myapp-spa-client
  &amp;redirect_uri=https://myapp.com/callback
  &amp;scope=read:profile
  &amp;state=xK9mP2qR7vL4nJ1w
  &amp;code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &amp;code_challenge_method=S256
HTTP/1.1
Host: auth.example.com
</span></code></pre></div></div>

<h5 id="step-3-token-exchange-with-pkce">Step 3: Token Exchange (with PKCE)</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>

grant_type=authorization_code
&amp;code=SplxlOBeZQQYbYS6WxSbIA
&amp;redirect_uri=https://myapp.com/callback
&amp;client_id=myapp-spa-client
&amp;code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
</code></pre></div></div>

<blockquote>
  <p>Note: No <code class="language-plaintext highlighter-rouge">client_secret</code> — the <code class="language-plaintext highlighter-rouge">code_verifier</code> serves as proof of ownership.</p>
</blockquote>

<p>The AS recomputes <code class="language-plaintext highlighter-rouge">SHA256(code_verifier)</code> and compares it to the stored <code class="language-plaintext highlighter-rouge">code_challenge</code>. If they match, it issues tokens.</p>

<p>This flow describes it properly, where client is our server side application.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+--------+          +----------+          +--------------------+          +--------+
|  User  |          | Browser  |          | Client (SP)        |          |  Auth  |
|        |          |          |          |                    |          | Server |
+--------+          +----------+          +--------------------+          +--------+
    |                    |                          |                         |
    | clicks login       |                          |                         |
    |-------------------&gt;|                          |                         |
    |                    | GET /login               |                         |
    |                    |-------------------------&gt;|                         |
    |                    |                          |                         |
    |                    |                          | gen code_verifier       |
    |                    |                          | gen code_challenge      |
    |                    |                          | = BASE64URL(SHA256(v))  |
    |                    |                          | gen state               |
    |                    |                          |                         |
    |                    |    302 → /authorize      |                         |
    |                    |  ?response_type=code     |                         |
    |                    |  &amp;code_challenge=...     |                         |
    |                    |  &amp;code_challenge_method  |                         |
    |                    |  =S256 &amp;state=...        |                         |
    |                    |&lt;-------------------------|                         |
    |                    |                                                    |
    |                    | GET /authorize?code_challenge=...&amp;state=...        |
    |                    |---------------------------------------------------&gt;|
    |                    |                                                    |
    |                    |              login + consent UI                    |
    |                    |&lt;---------------------------------------------------|
    | enter credentials  |                                                    |
    |  &amp; approve         |                                                    |
    |-------------------&gt;|                                                    |
    |                    | credentials                                        |
    |                    |---------------------------------------------------&gt;|
    |                    |                          |                         |
    |                    |                          |          store          |
    |                    |                          |       code_challenge    |
    |                    |                          |       against code      |
    |                    |                          |                         |
    |                    | 302 → /callback?code=AUTH_CODE&amp;state=...           |
    |                    |&lt;---------------------------------------------------|
    |                    | GET /callback?code=AUTH_CODE&amp;state=...             |
    |                    |-------------------------&gt;|                         |
    |                    |                          | verify state ✓          |
    |                    |                          |                         |
    |                    |                          | POST /token             |
    |                    |                          | grant_type=auth_code    |
    |                    |                          | &amp;code=AUTH_CODE         |
    |                    |                          | &amp;code_verifier=...      |
    |                    |                          |------------------------&gt;|
    |                    |                          |                         |
    |                    |                          |    SHA256(verifier)     |
    |                    |                          |    == code_challenge? ✓ |
    |                    |                          |                         |
    |                    |                          |   access_token          |
    |                    |                          |   id_token              |
    |                    |                          |   refresh_token         |
    |                    |                          |&lt;------------------------|
    |                    |    set session cookie    |                         |
    |                    |&lt;-------------------------|                         |
    | authenticated ✓    |                          |                         |
    |&lt;-------------------|                          |                         |
</code></pre></div></div>

<hr />

<h4 id="33-client-credentials-flow">3.3 Client Credentials Flow</h4>

<p><strong>Best for:</strong> Machine-to-machine (M2M) communication with <strong>no user involved</strong>.</p>

<p>Used by: microservices, background jobs, CI/CD pipelines, daemons.</p>

<h5 id="how-it-works">How It Works</h5>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client (Service)          Authorization Server          Resource Server
      |                          |                           |
      |-- POST /token ----------&gt;|                           |
      |   (client_id +           |                           |
      |    client_secret)        |                           |
      |&lt;-- access_token ---------|                           |
      |                          |                           |
      |-- API request + Bearer token -----------------------&gt;|
      |&lt;-- protected resource --------------------------------|
</code></pre></div></div>

<h5 id="token-request">Token Request</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Basic base64(client_id:client_secret)</span>

grant_type=client_credentials
&amp;scope=reports:read metrics:write
</code></pre></div></div>

<h5 id="token-response">Token Response</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiJ9..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"reports:read metrics:write"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>No <code class="language-plaintext highlighter-rouge">refresh_token</code> is issued — the client simply requests a new access token when needed.</p>
</blockquote>

<hr />

<h4 id="34-device-code-flow">3.4 Device Code Flow</h4>

<p><strong>Best for:</strong> Devices with limited input capability (smart TVs, IoT devices, CLI tools, gaming consoles).</p>

<p>The device cannot open a browser or accept keyboard input, so authentication is delegated to another device (e.g., a phone or laptop).</p>

<h5 id="how-it-works-1">How It Works</h5>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Device                    Auth Server                  User's Phone/Browser
  |                           |                                |
  |-- POST /device_code -----&gt;|                                |
  |&lt;-- device_code,           |                                |
  |    user_code,             |                                |
  |    verification_uri ------|                                |
  |                           |                                |
  |-- display user_code ------|-------- user visits URI ------&gt;|
  |                           |&lt;------- enters user_code ------|
  |                           |&lt;------- user approves ---------|
  |                           |                                |
  |-- poll POST /token ------&gt;|                                |
  |&lt;-- access_token ----------|                                |
</code></pre></div></div>

<h5 id="step-1-device-authorization-request">Step 1: Device Authorization Request</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/device_authorization</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>

client_id=device-client-id
&amp;scope=read:profile
</code></pre></div></div>

<h5 id="step-2-device-authorization-response">Step 2: Device Authorization Response</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"device_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"user_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"WDJB-MJHT"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"verification_uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/device"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"verification_uri_complete"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/device?user_code=WDJB-MJHT"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">900</span><span class="p">,</span><span class="w">
  </span><span class="nl">"interval"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The device displays: <em>“Go to auth.example.com/device and enter code: WDJB-MJHT”</em></p>

<h5 id="step-3-polling-for-the-token">Step 3: Polling for the Token</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>

grant_type=urn:ietf:params:oauth:grant-type:device_code
&amp;device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&amp;client_id=device-client-id
</code></pre></div></div>

<p>While the user hasn’t approved yet, the AS responds:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="s2">"authorization_pending"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Once the user approves, the next poll returns:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiJ9..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8xLOxBtZp8"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h4 id="35-refresh-token-flow">3.5 Refresh Token Flow</h4>

<p>Access tokens expire. Rather than forcing the user to re-authenticate, the client uses a <strong>refresh token</strong> to get a new access token silently.</p>

<h5 id="token-refresh-request">Token Refresh Request</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Basic base64(client_id:client_secret)</span>

grant_type=refresh_token
&amp;refresh_token=8xLOxBtZp8
</code></pre></div></div>

<h5 id="token-refresh-response">Token Refresh Response</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiJ9.NEW..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9yMPxCuAq9"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>Some AS implementations use <strong>refresh token rotation</strong> — each refresh issues a new refresh token and invalidates the old one. This limits the damage if a refresh token is stolen.</p>
</blockquote>

<hr />

<h4 id="36-deprecated-flows-avoid-these">3.6 Deprecated Flows (Avoid These)</h4>

<h5 id="implicit-flow-deprecated">Implicit Flow (DEPRECATED)</h5>

<p>The Authorization Server returned the access token <strong>directly in the URL fragment</strong> after user approval:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://myapp.com/callback#access_token=eyJ...&amp;expires_in=3600
</code></pre></div></div>

<p><strong>Why it’s dangerous and deprecated:</strong></p>

<ul>
  <li>Access token appears in the browser URL bar — visible in browser history, server logs, and <code class="language-plaintext highlighter-rouge">Referer</code> headers</li>
  <li>No <code class="language-plaintext highlighter-rouge">state</code> parameter verification was common, enabling CSRF</li>
  <li>No refresh token support</li>
  <li>Replaced entirely by <strong>Authorization Code + PKCE</strong> for public clients</li>
</ul>

<h5 id="resource-owner-password-credentials-ropc-deprecated">Resource Owner Password Credentials (ROPC) (DEPRECATED)</h5>

<p>The user gives their username and password <strong>directly to the client application</strong>, which passes them to the AS.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST /token
grant_type=password&amp;username=user@example.com&amp;password=hunter2&amp;client_id=...
</span></code></pre></div></div>

<p><strong>Why it’s dangerous:</strong></p>

<ul>
  <li>Completely defeats the purpose of OAuth (the client sees credentials)</li>
  <li>No consent screen or scoped access</li>
  <li>Only ever acceptable for first-party apps during migration — even then, avoid it</li>
</ul>

<hr />

<h3 id="4-protocol-internals">4. Protocol Internals</h3>

<h4 id="41-key-endpoints">4.1 Key Endpoints</h4>

<h5 id="authorize-get"><code class="language-plaintext highlighter-rouge">/authorize</code> (GET)</h5>

<p>Starts user-facing authorization. Browser redirects here.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET https://auth.example.com/authorize?response_type=code&amp;client_id=...&amp;...
</code></pre></div></div>

<p>This is where the user logs in and sees the consent screen.</p>

<h5 id="token-post"><code class="language-plaintext highlighter-rouge">/token</code> (POST)</h5>

<p>Back-channel token issuance. Called by the client application directly (not browser redirect).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&amp;code=...&amp;...
</code></pre></div></div>

<h5 id="well-knownopenid-configuration-get"><code class="language-plaintext highlighter-rouge">/.well-known/openid-configuration</code> (GET)</h5>

<p>OIDC Discovery Document — a JSON document listing all AS endpoints, supported scopes, signing algorithms, etc. Use this to configure OAuth clients dynamically.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://accounts.google.com/.well-known/openid-configuration
</code></pre></div></div>

<h4 id="42-key-parameters">4.2 Key Parameters</h4>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Where Used</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">client_id</code></td>
      <td>All requests</td>
      <td>Identifies the application</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">client_secret</code></td>
      <td>Token requests (confidential clients)</td>
      <td>Authenticates the application</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">redirect_uri</code></td>
      <td>Auth request, token request</td>
      <td>Where to return the user; must match registered value</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">response_type</code></td>
      <td>Auth request</td>
      <td><code class="language-plaintext highlighter-rouge">code</code> for Authorization Code flow</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scope</code></td>
      <td>Auth request</td>
      <td>Permissions being requested (space-separated)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">state</code></td>
      <td>Auth request, response</td>
      <td>CSRF protection — random, opaque, verified on return</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code</code></td>
      <td>Auth response, token request</td>
      <td>Short-lived authorization code</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">grant_type</code></td>
      <td>Token request</td>
      <td>The flow being used</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code_verifier</code></td>
      <td>Token request (PKCE)</td>
      <td>Proves the client made the original auth request</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code_challenge</code></td>
      <td>Auth request (PKCE)</td>
      <td>Hash of the code_verifier</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nonce</code></td>
      <td>Auth request (OIDC)</td>
      <td>Binds ID token to specific auth session, prevents replay</td>
    </tr>
  </tbody>
</table>

<h4 id="43-scopes-and-consent">4.3 Scopes and Consent</h4>

<p>Scopes are <strong>string identifiers</strong> representing permissions. There is no universal standard for scope names (except OIDC scopes like <code class="language-plaintext highlighter-rouge">openid</code>, <code class="language-plaintext highlighter-rouge">profile</code>, <code class="language-plaintext highlighter-rouge">email</code>).</p>

<p>Examples:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openid profile email          # OIDC identity scopes
read:repositories             # GitHub
https://www.googleapis.com/auth/gmail.readonly   # Google (URL-style scopes)
user.read Mail.ReadWrite      # Microsoft Graph
</code></pre></div></div>

<p>The consent screen shown to users maps scopes to human-readable descriptions. Requesting minimal, specific scopes is a security best practice (<strong>principle of least privilege</strong>).</p>

<h4 id="44-token-formats">4.4 Token Formats</h4>

<h5 id="opaque-tokens">Opaque Tokens</h5>

<ul>
  <li>A random string with no inherent meaning</li>
  <li>The Resource Server must call the AS’s <strong>introspection endpoint</strong> (<code class="language-plaintext highlighter-rouge">/introspect</code>) to validate</li>
  <li>Allows the AS to revoke tokens instantly</li>
  <li>More network overhead</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Introspection request</span>
POST /introspect
Authorization: Basic <span class="nb">base64</span><span class="o">(</span>rs_client_id:rs_secret<span class="o">)</span>
<span class="nv">token</span><span class="o">=</span>random_opaque_token_string
</code></pre></div></div>

<h5 id="jwt-json-web-token">JWT (JSON Web Token)</h5>

<p>Defined in <a href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519</a>. Self-contained: the Resource Server can validate without calling the AS.</p>

<p>Structure: <code class="language-plaintext highlighter-rouge">header.payload.signature</code> (each Base64URL encoded)</p>

<p><strong>Header:</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RS256"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"typ"</span><span class="p">:</span><span class="w"> </span><span class="s2">"JWT"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"kid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"key-id-1"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Payload (Claims):</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"iss"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user-12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"aud"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1700000000</span><span class="p">,</span><span class="w">
  </span><span class="nl">"iat"</span><span class="p">:</span><span class="w"> </span><span class="mi">1699996400</span><span class="p">,</span><span class="w">
  </span><span class="nl">"jti"</span><span class="p">:</span><span class="w"> </span><span class="s2">"unique-token-id"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"read:profile read:email"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Signature:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RSA_SHA256(
  base64url(header) + "." + base64url(payload),
  private_key
)
</code></pre></div></div>

<h4 id="45-token-validation-jwt">4.5 Token Validation (JWT)</h4>

<p>A Resource Server validating a JWT must check:</p>

<ol>
  <li><strong>Signature</strong> — Verify using the AS’s public key (fetched from <code class="language-plaintext highlighter-rouge">/jwks</code> endpoint)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">iss</code> (issuer)</strong> — Must match the expected Authorization Server</li>
  <li><strong><code class="language-plaintext highlighter-rouge">aud</code> (audience)</strong> — Must include this Resource Server’s identifier</li>
  <li><strong><code class="language-plaintext highlighter-rouge">exp</code> (expiry)</strong> — Current time must be before expiry</li>
  <li><strong><code class="language-plaintext highlighter-rouge">iat</code> (issued at)</strong> — Should not be too far in the past</li>
  <li><strong><code class="language-plaintext highlighter-rouge">scope</code></strong> — Token must contain the required scope for the requested operation</li>
  <li><strong><code class="language-plaintext highlighter-rouge">jti</code> (JWT ID)</strong> — Optionally check against a blocklist for replay prevention</li>
</ol>

<hr />

<h3 id="5-end-to-end-example">5. End-to-End Example</h3>

<p>A complete Authorization Code + PKCE flow for a Single Page Application calling a GitHub-style API.</p>

<h4 id="setup">Setup</h4>

<ul>
  <li><strong>Client:</strong> Single Page Application at <code class="language-plaintext highlighter-rouge">https://myapp.com</code></li>
  <li><strong>Authorization Server:</strong> <code class="language-plaintext highlighter-rouge">https://auth.example.com</code></li>
  <li><strong>Resource Server:</strong> <code class="language-plaintext highlighter-rouge">https://api.example.com</code></li>
  <li><strong>Requested scope:</strong> <code class="language-plaintext highlighter-rouge">repo:read user:profile</code></li>
</ul>

<hr />

<h4 id="step-1-user-clicks-connect-account">Step 1: User Clicks “Connect Account”</h4>

<p>The SPA generates PKCE values:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code_verifier  = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = BASE64URL(SHA256(code_verifier))
             = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
state          = "abc123xyz789"   ← stored in sessionStorage
</code></pre></div></div>

<p><strong>Browser is redirected to:</strong></p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?
  response_type=code
  &amp;client_id=spa-client-001
  &amp;redirect_uri=https://myapp.com/callback
  &amp;scope=repo:read%20user:profile
  &amp;state=abc123xyz789
  &amp;code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &amp;code_challenge_method=S256
HTTP/1.1
Host: auth.example.com
</span></code></pre></div></div>

<blockquote>
  <p><strong>What happens:</strong> AS presents login page and consent screen to user.</p>
</blockquote>

<hr />

<h4 id="step-2-user-authenticates-and-approves">Step 2: User Authenticates and Approves</h4>

<p>After the user logs in and clicks “Allow,” the AS redirects back to the SPA:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">302</span> <span class="ne">Found</span>
<span class="na">Location</span><span class="p">:</span> <span class="s">https://myapp.com/callback?</span>
<span class="s">  code=SplxlOBeZQQYbYS6WxSbIA</span>
<span class="s">  &amp;state=abc123xyz789</span>
</code></pre></div></div>

<p><strong>SPA verifies:</strong> <code class="language-plaintext highlighter-rouge">state</code> in the redirect matches <code class="language-plaintext highlighter-rouge">state</code> stored in sessionStorage.</p>

<blockquote>
  <p><strong>What happens:</strong> SPA now has an authorization code. This code is useless to an attacker without the <code class="language-plaintext highlighter-rouge">code_verifier</code>.</p>
</blockquote>

<hr />

<h4 id="step-3-spa-exchanges-code-for-tokens">Step 3: SPA Exchanges Code for Tokens</h4>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>

grant_type=authorization_code
&amp;code=SplxlOBeZQQYbYS6WxSbIA
&amp;redirect_uri=https://myapp.com/callback
&amp;client_id=spa-client-001
&amp;code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
</code></pre></div></div>

<blockquote>
  <p><strong>What the AS does:</strong> Recomputes <code class="language-plaintext highlighter-rouge">SHA256(code_verifier)</code>, compares to stored <code class="language-plaintext highlighter-rouge">code_challenge</code>. Match → issues tokens.</p>
</blockquote>

<p><strong>Response:</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLTEyMzQ1IiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJleHAiOjE3MDAwMDAzNjAsImlhdCI6MTcwMDAwMDAwMCwic2NvcGUiOiJyZXBvOnJlYWQgdXNlcjpwcm9maWxlIn0.SIGNATURE"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8xLOxBtZp8"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"repo:read user:profile"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h4 id="step-4-spa-calls-the-api">Step 4: SPA Calls the API</h4>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/user/profile</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">api.example.com</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIn0...</span>
</code></pre></div></div>

<blockquote>
  <p><strong>What the RS does:</strong> Validates JWT signature, checks <code class="language-plaintext highlighter-rouge">iss</code>, <code class="language-plaintext highlighter-rouge">aud</code>, <code class="language-plaintext highlighter-rouge">exp</code>, <code class="language-plaintext highlighter-rouge">scope</code>. All valid → returns data.</p>
</blockquote>

<p><strong>Response:</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user-12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shaheryar"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shaheryar@example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"repos"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="err">...</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h4 id="step-5-access-token-expires--spa-refreshes">Step 5: Access Token Expires — SPA Refreshes</h4>

<p>One hour later, the API returns <code class="language-plaintext highlighter-rouge">401 Unauthorized</code>. The SPA uses the refresh token:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>

grant_type=refresh_token
&amp;refresh_token=8xLOxBtZp8
&amp;client_id=spa-client-001
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiJ9.NEW_TOKEN..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9yMPxCuAq9"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The user never had to log in again.</p>

<hr />

<h3 id="6-practical-exercises">6. Practical Exercises</h3>

<h4 id="exercise-1-craft-an-authorization-request-in-the-browser">Exercise 1: Craft an Authorization Request in the Browser</h4>

<p>Use Google’s OAuth 2.0 Playground or construct this URL manually and open it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://accounts.google.com/o/oauth2/v2/auth?
  response_type=code
  &amp;client_id=YOUR_CLIENT_ID
  &amp;redirect_uri=https://oauth2.example.com/code
  &amp;scope=openid%20email%20profile
  &amp;state=random_state_value_here
  &amp;access_type=offline
</code></pre></div></div>

<p><strong>Observe:</strong></p>

<ul>
  <li>The Google login/consent screen</li>
  <li>The <code class="language-plaintext highlighter-rouge">code</code> parameter in the redirect URL after approval</li>
  <li>The <code class="language-plaintext highlighter-rouge">state</code> value echoed back</li>
</ul>

<hr />

<h4 id="exercise-2-exchange-a-code-for-a-token-using-curl">Exercise 2: Exchange a Code for a Token Using curl</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST https://oauth2.googleapis.com/token <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/x-www-form-urlencoded"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"grant_type=authorization_code"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"code=YOUR_AUTHORIZATION_CODE"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"client_id=YOUR_CLIENT_ID"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"client_secret=YOUR_CLIENT_SECRET"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"redirect_uri=https://oauth2.example.com/code"</span>
</code></pre></div></div>

<p><strong>Or with PowerShell:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">grant_type</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s2">"authorization_code"</span><span class="w">
    </span><span class="nx">code</span><span class="w">          </span><span class="o">=</span><span class="w"> </span><span class="s2">"YOUR_AUTHORIZATION_CODE"</span><span class="w">
    </span><span class="nx">client_id</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s2">"YOUR_CLIENT_ID"</span><span class="w">
    </span><span class="nx">client_secret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"YOUR_CLIENT_SECRET"</span><span class="w">
    </span><span class="nx">redirect_uri</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://oauth2.example.com/code"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">Post</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"https://oauth2.googleapis.com/token"</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$body</span><span class="w">
</span></code></pre></div></div>

<hr />

<h4 id="exercise-3-decode-and-inspect-a-jwt">Exercise 3: Decode and Inspect a JWT</h4>

<p>Take any JWT (e.g., the <code class="language-plaintext highlighter-rouge">access_token</code> from the above exercise) and decode it. Use <a href="https://jwt.io">jwt.io</a> in the browser, or via CLI:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Split JWT by "." and decode each part</span>
<span class="nv">TOKEN</span><span class="o">=</span><span class="s2">"eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyLTEyMzQ1In0.SIGNATURE"</span>

<span class="c"># Decode header</span>
<span class="nb">echo</span> <span class="nv">$TOKEN</span> | <span class="nb">cut</span> <span class="nt">-d</span><span class="nb">.</span> <span class="nt">-f1</span> | <span class="nb">base64</span> <span class="nt">-d</span> 2&gt;/dev/null | python3 <span class="nt">-m</span> json.tool

<span class="c"># Decode payload</span>
<span class="nb">echo</span> <span class="nv">$TOKEN</span> | <span class="nb">cut</span> <span class="nt">-d</span><span class="nb">.</span> <span class="nt">-f2</span> | <span class="nb">base64</span> <span class="nt">-d</span> 2&gt;/dev/null | python3 <span class="nt">-m</span> json.tool
</code></pre></div></div>

<p><strong>Identify:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">iss</code> — who issued the token</li>
  <li><code class="language-plaintext highlighter-rouge">aud</code> — who the token is for</li>
  <li><code class="language-plaintext highlighter-rouge">exp</code> — when it expires (Unix timestamp → <code class="language-plaintext highlighter-rouge">date -d @TIMESTAMP</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">scope</code> — what permissions it grants</li>
</ul>

<blockquote>
  <p><strong>Note:</strong> You can decode the header and payload without any keys — they’re just Base64. The signature is what you need the public key to verify.</p>
</blockquote>

<hr />

<h4 id="exercise-4-identify-parameters-in-a-real-oauth-request">Exercise 4: Identify Parameters in a Real OAuth Request</h4>

<p>Capture an OAuth flow using browser DevTools (Network tab) when logging in with Google on any site. Find and identify:</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />The <code class="language-plaintext highlighter-rouge">GET /authorize</code> request and all query parameters</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />The <code class="language-plaintext highlighter-rouge">state</code> value sent and returned</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />The <code class="language-plaintext highlighter-rouge">scope</code> being requested</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />The <code class="language-plaintext highlighter-rouge">redirect_uri</code> — does it match exactly?</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />The <code class="language-plaintext highlighter-rouge">POST /token</code> request (may be hidden — it’s server-side)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Whether PKCE parameters are present (<code class="language-plaintext highlighter-rouge">code_challenge</code>, <code class="language-plaintext highlighter-rouge">code_challenge_method</code>)</li>
</ul>

<hr />

<h3 id="7-security-deep-dive">7. Security Deep Dive</h3>

<p>This section covers OAuth 2.0 attack vectors, how they work, and how to prevent them. Understanding these is essential for SC-200 and IAM security roles.</p>

<hr />

<h4 id="71-authorization-code-interception">7.1 Authorization Code Interception</h4>

<h5 id="the-attack">The Attack</h5>

<p>An attacker intercepts the authorization code from the redirect URL before the legitimate client receives it. This can happen via:</p>

<ul>
  <li>Malicious apps on a mobile device registered to handle the same URI scheme</li>
  <li>Compromised browser extensions</li>
  <li>Network interception if redirect URI uses HTTP</li>
</ul>

<p><strong>Attack scenario:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. User approves consent on legitimate app
2. AS redirects: myapp://callback?code=STOLEN_CODE
3. Attacker's malicious app intercepts the redirect (registered same URI)
4. Attacker sends STOLEN_CODE to AS /token endpoint
5. Attacker receives valid access token
</code></pre></div></div>

<h5 id="mitigation">Mitigation</h5>

<p><strong>PKCE</strong> — Even with the code, the attacker cannot exchange it for a token without the <code class="language-plaintext highlighter-rouge">code_verifier</code> that was never sent over the network unencrypted. Always use PKCE for public clients.</p>

<hr />

<h4 id="72-missing-or-weak-state-parameter-csrf">7.2 Missing or Weak <code class="language-plaintext highlighter-rouge">state</code> Parameter (CSRF)</h4>

<h5 id="the-attack-1">The Attack</h5>

<p>If the <code class="language-plaintext highlighter-rouge">state</code> parameter is absent or not validated, the flow is vulnerable to Cross-Site Request Forgery.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Attacker begins an OAuth flow and gets a valid authorization URL
2. Attacker sends victim a link that completes the attacker's authorization
3. Victim's browser sends the attacker's authorization code to the victim's logged-in session
4. Victim's account is now linked to attacker's external account
5. Attacker can now authenticate as victim
</code></pre></div></div>

<p>This is called an <strong>OAuth CSRF</strong> or <strong>account linking attack</strong>.</p>

<h5 id="mitigation-1">Mitigation</h5>

<ul>
  <li>Always generate a <strong>cryptographically random</strong> <code class="language-plaintext highlighter-rouge">state</code> value per authorization request</li>
  <li>Store it server-side or in a secure, HttpOnly cookie</li>
  <li><strong>Reject any callback where <code class="language-plaintext highlighter-rouge">state</code> doesn’t match the stored value</strong></li>
  <li>Never use predictable values (<code class="language-plaintext highlighter-rouge">state=1</code>, <code class="language-plaintext highlighter-rouge">state=userid</code>)</li>
</ul>

<hr />

<h4 id="73-open-redirect-abuse">7.3 Open Redirect Abuse</h4>

<h5 id="the-attack-2">The Attack</h5>

<p>If the AS doesn’t strictly validate <code class="language-plaintext highlighter-rouge">redirect_uri</code>, an attacker can steal the authorization code by redirecting it to a URL they control:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Attacker crafts:
GET /authorize?client_id=legit-app&amp;redirect_uri=https://attacker.com/steal&amp;...

If AS allows partial/prefix matching:
→ Auth code is delivered to attacker.com
</code></pre></div></div>

<p>Partial matching is the key vulnerability: if the AS checks only that the <code class="language-plaintext highlighter-rouge">redirect_uri</code> <em>starts with</em> the registered domain, <code class="language-plaintext highlighter-rouge">https://legit.com.evil.com</code> would pass.</p>

<h5 id="mitigation-2">Mitigation</h5>

<ul>
  <li><strong>Exact string matching</strong> of <code class="language-plaintext highlighter-rouge">redirect_uri</code> against pre-registered values — no wildcards, no prefix matching</li>
  <li>Require redirect URIs to be registered explicitly at client registration</li>
  <li>The AS should reject any mismatch with an error (not silently redirect)</li>
</ul>

<hr />

<h4 id="74-token-leakage">7.4 Token Leakage</h4>

<h5 id="the-attack-3">The Attack</h5>

<p>Access tokens exposed in:</p>

<ul>
  <li><strong>URL query strings</strong> — appear in browser history, server access logs, proxy logs, and <code class="language-plaintext highlighter-rouge">Referer</code> headers (<code class="language-plaintext highlighter-rouge">?access_token=eyJ...</code>)</li>
  <li><strong>Browser history</strong> — Implicit Flow’s fatal flaw</li>
  <li><strong>Log files</strong> — if tokens are logged in application logs</li>
  <li><strong>Referer headers</strong> — when navigating from a page with a token in the URL</li>
</ul>

<h5 id="mitigation-3">Mitigation</h5>

<ul>
  <li><strong>Never</strong> put tokens in URL query parameters</li>
  <li>Use <code class="language-plaintext highlighter-rouge">Authorization: Bearer &lt;token&gt;</code> header exclusively</li>
  <li>Set <code class="language-plaintext highlighter-rouge">Referrer-Policy: no-referrer</code> on pages that handle tokens</li>
  <li>Ensure tokens are excluded from application and infrastructure logging</li>
  <li>Use short token lifetimes to limit the window of exposure</li>
</ul>

<hr />

<h4 id="75-pkce-bypass-and-misuse">7.5 PKCE Bypass and Misuse</h4>

<h5 id="the-attack-4">The Attack</h5>

<ol>
  <li><strong>No PKCE enforcement:</strong> AS accepts token requests without <code class="language-plaintext highlighter-rouge">code_verifier</code> even when PKCE was used in the auth request → PKCE is decorative</li>
  <li><strong><code class="language-plaintext highlighter-rouge">code_challenge_method=plain</code>:</strong> Using the verifier as the challenge directly — if an attacker can intercept the auth request, they get the verifier</li>
  <li><strong>Weak code_verifier:</strong> Using a short or low-entropy verifier — vulnerable to brute force</li>
</ol>

<h5 id="mitigation-4">Mitigation</h5>

<ul>
  <li>AS must <strong>require</strong> <code class="language-plaintext highlighter-rouge">code_verifier</code> if <code class="language-plaintext highlighter-rouge">code_challenge</code> was sent</li>
  <li><strong>Only allow <code class="language-plaintext highlighter-rouge">S256</code></strong> as <code class="language-plaintext highlighter-rouge">code_challenge_method</code> — reject <code class="language-plaintext highlighter-rouge">plain</code></li>
  <li>Enforce minimum code_verifier length (43 characters) and cryptographic randomness</li>
  <li>PKCE should be required for all public clients and recommended for confidential clients too</li>
</ul>

<hr />

<h4 id="76-improper-redirect-uri-validation">7.6 Improper Redirect URI Validation</h4>

<p>Already partly covered in Open Redirect, but includes additional vectors:</p>

<ul>
  <li><strong>Subdomain matching:</strong> Allowing <code class="language-plaintext highlighter-rouge">*.example.com</code> — attacker registers <code class="language-plaintext highlighter-rouge">evil.example.com</code></li>
  <li><strong>Path traversal:</strong> <code class="language-plaintext highlighter-rouge">https://legit.com/path/../../../</code> tricks</li>
  <li><strong>Localhost exceptions:</strong> Some AS implementations allow any localhost port — attackers run a local service to catch redirects</li>
  <li><strong>HTTP vs HTTPS:</strong> Allowing HTTP redirect URIs in production</li>
</ul>

<h5 id="mitigation-5">Mitigation</h5>

<ul>
  <li>Exact string comparison only</li>
  <li>All production redirect URIs must use HTTPS</li>
  <li>Reject loopback (localhost) URIs in production except for native app flows with specific protections</li>
</ul>

<hr />

<h4 id="77-scope-over-permission">7.7 Scope Over-Permission</h4>

<h5 id="the-attack-5">The Attack</h5>

<ul>
  <li>Applications requesting <code class="language-plaintext highlighter-rouge">*</code> or <code class="language-plaintext highlighter-rouge">admin</code> scopes when they only need read access</li>
  <li>Users blindly approving broad scopes they don’t understand</li>
  <li>If the application is compromised, the attacker has access to far more than necessary</li>
</ul>

<h5 id="mitigation-6">Mitigation</h5>

<ul>
  <li>Request <strong>minimum necessary scopes</strong> for the specific operation</li>
  <li>Implement <strong>incremental authorization</strong> — request additional scopes only when needed</li>
  <li>AS should present clear, human-readable descriptions of each scope</li>
  <li>Resource Server should verify the <strong>specific scope</strong> required for each endpoint, not just “is the token valid?”</li>
</ul>

<hr />

<h4 id="78-token-replay-attacks">7.8 Token Replay Attacks</h4>

<h5 id="the-attack-6">The Attack</h5>

<p>If an attacker obtains a valid access token (via leakage, network interception, etc.), they can replay it against the Resource Server for the duration of its lifetime.</p>

<h5 id="mitigation-7">Mitigation</h5>

<ul>
  <li><strong>Short token lifetimes</strong> (15–60 minutes) limit the replay window</li>
  <li><strong><code class="language-plaintext highlighter-rouge">jti</code> (JWT ID) tracking</strong> — maintain a blocklist of used JTIs on the Resource Server</li>
  <li><strong>Sender-constrained tokens</strong> — Mutual TLS (mTLS) or DPoP (Demonstrating Proof of Possession) bind tokens to the client’s cryptographic key, making stolen tokens useless without the private key</li>
  <li><strong>Token revocation</strong> — AS implements a revocation endpoint; Resource Servers check it or use short lifetimes</li>
</ul>

<hr />

<h4 id="79-real-world-attack-scenario-account-takeover-via-oauth">7.9 Real-World Attack Scenario: Account Takeover via OAuth</h4>

<p><strong>Setup:</strong> A web app lets users “Login with Google.” The app links a Google account to a local account by matching email addresses.</p>

<p><strong>Attack:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Attacker knows victim's email: victim@gmail.com
2. Attacker creates a Google account with victim@gmail.com (or controls one)
3. Attacker completes OAuth login on the target app using their Google account
4. Target app sees email=victim@gmail.com and logs attacker into victim's account
</code></pre></div></div>

<p><strong>Root cause:</strong> Trusting email from an OAuth provider without verifying it’s been confirmed by that provider, or not checking the <code class="language-plaintext highlighter-rouge">sub</code> (user ID) which is unique and stable.</p>

<p><strong>Fix:</strong> Always use the immutable <code class="language-plaintext highlighter-rouge">sub</code> claim (not <code class="language-plaintext highlighter-rouge">email</code>) as the primary identifier for account matching. Email can change; <code class="language-plaintext highlighter-rouge">sub</code> cannot.</p>

<hr />

<h3 id="8-best-practices">8. Best Practices</h3>

<h4 id="81-flow-selection-guide">8.1 Flow Selection Guide</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Is a user present?
├── YES
│   ├── Is the client a public app (SPA, mobile)?
│   │   └── Authorization Code + PKCE ✓
│   ├── Is the client a server-side app?
│   │   └── Authorization Code (+ PKCE recommended) ✓
│   └── Is the device input-constrained (TV, CLI)?
│       └── Device Code Flow ✓
└── NO (machine-to-machine)
    └── Client Credentials Flow ✓

NEVER USE:
- Implicit Flow (deprecated)
- ROPC / Password Grant (deprecated)
</code></pre></div></div>

<h4 id="82-token-security">8.2 Token Security</h4>

<table>
  <thead>
    <tr>
      <th>Practice</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Short access token lifetime (15–60 min)</td>
      <td>Limits damage from token leakage</td>
    </tr>
    <tr>
      <td>Use refresh token rotation</td>
      <td>Detect stolen refresh tokens (reuse = alert)</td>
    </tr>
    <tr>
      <td>Store tokens in HttpOnly cookies or server-side</td>
      <td>Protects against XSS in SPAs</td>
    </tr>
    <tr>
      <td>Never store tokens in localStorage</td>
      <td>XSS can steal them</td>
    </tr>
    <tr>
      <td>Never log tokens</td>
      <td>Prevent leakage through log aggregators</td>
    </tr>
    <tr>
      <td>Use HTTPS everywhere</td>
      <td>Prevent interception</td>
    </tr>
  </tbody>
</table>

<h4 id="83-authorization-server-configuration">8.3 Authorization Server Configuration</h4>

<ul>
  <li>Enforce exact <code class="language-plaintext highlighter-rouge">redirect_uri</code> matching</li>
  <li>Require PKCE for all public clients</li>
  <li>Set maximum authorization code lifetime to 60 seconds</li>
  <li>Implement authorization code single-use enforcement</li>
  <li>Enable refresh token rotation and absolute expiration</li>
  <li>Publish a <code class="language-plaintext highlighter-rouge">/.well-known/openid-configuration</code> discovery document</li>
  <li>Rotate signing keys regularly and publish via JWKS endpoint</li>
</ul>

<h4 id="84-resource-server-implementation">8.4 Resource Server Implementation</h4>

<ul>
  <li>Validate <strong>every</strong> claim in the JWT: <code class="language-plaintext highlighter-rouge">iss</code>, <code class="language-plaintext highlighter-rouge">aud</code>, <code class="language-plaintext highlighter-rouge">exp</code>, <code class="language-plaintext highlighter-rouge">scope</code></li>
  <li>Verify the JWT signature using the AS’s public key from the JWKS endpoint (cache with TTL, don’t fetch on every request)</li>
  <li>Check the <strong>specific scope</strong> required for each API endpoint</li>
  <li>Return <code class="language-plaintext highlighter-rouge">WWW-Authenticate: Bearer error="insufficient_scope"</code> on scope failure</li>
  <li>Return <code class="language-plaintext highlighter-rouge">WWW-Authenticate: Bearer error="invalid_token"</code> on token failure</li>
</ul>

<h4 id="85-modern-recommendations-summary">8.5 Modern Recommendations Summary</h4>

<ul>
  <li>✅ <strong>Always use PKCE</strong> — even for confidential clients (defense in depth)</li>
  <li>✅ <strong>Use short-lived access tokens</strong> with refresh tokens</li>
  <li>✅ <strong>Use <code class="language-plaintext highlighter-rouge">state</code> parameter</strong> — always generate and validate it</li>
  <li>✅ <strong>Use <code class="language-plaintext highlighter-rouge">nonce</code></strong> (in OIDC flows) to prevent ID token replay</li>
  <li>✅ <strong>Request minimal scopes</strong> — principle of least privilege</li>
  <li>✅ <strong>Use exact redirect URI matching</strong> — no wildcards, no prefix matching</li>
  <li>✅ <strong>Implement token binding</strong> (DPoP or mTLS) for high-security environments</li>
  <li>✅ <strong>Use refresh token rotation</strong> — single-use refresh tokens</li>
  <li>❌ <strong>Never use Implicit Flow</strong></li>
  <li>❌ <strong>Never use ROPC/Password Grant</strong></li>
  <li>❌ <strong>Never put tokens in query strings</strong></li>
  <li>❌ <strong>Never store tokens in localStorage</strong> (use HttpOnly cookies or memory)</li>
</ul>

<hr />

<h3 id="quick-reference-oauth-20-at-a-glance">Quick Reference: OAuth 2.0 at a Glance</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACTORS:
  Resource Owner     → The user who owns the data
  Client             → The app wanting access
  Authorization Server → Issues tokens (Google, Azure AD, Okta...)
  Resource Server    → The API holding the protected data

TOKENS:
  Access Token       → Short-lived, sent to APIs (Bearer header)
  Refresh Token      → Long-lived, gets new access tokens
  ID Token (OIDC)    → JWT with user identity claims

FLOWS:
  Auth Code + PKCE   → User-facing flows (preferred for all clients)
  Client Credentials → M2M, no user
  Device Code        → Input-constrained devices
  Refresh Token      → Silent token renewal

SECURITY MUSTS:
  ✓ PKCE
  ✓ state parameter
  ✓ Exact redirect_uri matching
  ✓ Short token lifetimes
  ✓ HTTPS everywhere
  ✓ Validate ALL JWT claims
</code></pre></div></div>

<hr />

<h3 id="references">References</h3>

<ul>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc6749">RFC 6749 — The OAuth 2.0 Authorization Framework</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc7636">RFC 7636 — PKCE for OAuth Public Clients</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519 — JSON Web Token (JWT)</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc8628">RFC 8628 — OAuth 2.0 Device Authorization Grant</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc9068">RFC 9068 — JWT Profile for OAuth 2.0 Access Tokens</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics">OAuth 2.0 Security Best Current Practice (BCP)</a></li>
  <li><a href="https://owasp.org/www-project-web-security-testing-guide/">OWASP — Testing for OAuth Weaknesses</a></li>
  <li><a href="https://jwt.io">jwt.io — JWT Debugger</a></li>
  <li><a href="https://developers.google.com/oauthplayground">OAuth 2.0 Playground (Google)</a></li>
</ul>]]></content><author><name>shaheerkj</name></author><category term="Security" /><category term="Cloud Security" /><category term="oauth2.0" /><category term="authorization" /><category term="IAM" /><category term="IAM" /><category term="Entra ID" /><category term="Identity Management" /><summary type="html"><![CDATA[This post explores the identity and authentication protocol that support modern day web infrastructure]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.shaheerkj.me/%7B%22path%22=%3Enil%7D" /><media:content medium="image" url="https://blog.shaheerkj.me/%7B%22path%22=%3Enil%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Open ID Connect Protocol (OIDC)</title><link href="https://blog.shaheerkj.me/posts/OIDC-deep-dive/" rel="alternate" type="text/html" title="Open ID Connect Protocol (OIDC)" /><published>2026-04-29T00:00:00+00:00</published><updated>2026-04-29T18:55:50+00:00</updated><id>https://blog.shaheerkj.me/posts/OIDC-deep-dive</id><content type="html" xml:base="https://blog.shaheerkj.me/posts/OIDC-deep-dive/"><![CDATA[<h2 id="openid-connect-oidc-a-technical-deep-dive">OpenID Connect (OIDC): A Technical Deep Dive</h2>

<h3 id="1-context-and-positioning">1. Context and Positioning</h3>

<h4 id="11-oidc-as-a-layer-not-a-replacement">1.1 OIDC as a Layer, Not a Replacement</h4>

<p>OpenID Connect (<a href="https://openid.net/specs/openid-connect-core-1_0.html">specification</a>) is a thin <strong>identity layer</strong> built on top of OAuth 2.0. It does not replace OAuth 2.0 — it extends it by answering the question OAuth deliberately avoided:</p>

<blockquote>
  <p><em>“Who is the user that just authorized this request?”</em></p>
</blockquote>

<p>The extension is minimal by design:</p>

<ul>
  <li>Adds a new token type: the <strong>ID Token</strong> (a signed JWT asserting user identity)</li>
  <li>Adds a new scope: <code class="language-plaintext highlighter-rouge">openid</code> (signals to the AS that OIDC is being used)</li>
  <li>Adds a new endpoint: <strong>UserInfo</strong> (for fetching additional identity claims)</li>
  <li>Adds new parameters: <code class="language-plaintext highlighter-rouge">nonce</code>, <code class="language-plaintext highlighter-rouge">prompt</code>, <code class="language-plaintext highlighter-rouge">max_age</code>, <code class="language-plaintext highlighter-rouge">acr_values</code>, <code class="language-plaintext highlighter-rouge">login_hint</code></li>
  <li>Standardizes a <strong>discovery document</strong> and <strong>JWKS endpoint</strong></li>
</ul>

<p>Every other mechanism — flows, token endpoint, PKCE, redirect URIs, client registration — is inherited from OAuth 2.0 unchanged.</p>

<h4 id="12-the-protocol-boundary">1.2 The Protocol Boundary</h4>

<p>The separation is precise:</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Protocol</th>
      <th>Token</th>
      <th>Question Answered</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Authorization</td>
      <td>OAuth 2.0</td>
      <td>Access Token</td>
      <td>Is this client allowed to do X?</td>
    </tr>
    <tr>
      <td>Authentication</td>
      <td>OIDC</td>
      <td>ID Token</td>
      <td>Who is the authenticated user?</td>
    </tr>
  </tbody>
</table>

<p>This boundary is not cosmetic. Confusing the two — using an access token as proof of identity, or sending an ID Token to an API — is a significant security mistake covered in Section 9.</p>

<h4 id="13-adoption-and-ecosystem">1.3 Adoption and Ecosystem</h4>

<p>OIDC is the dominant federated identity protocol for modern web, mobile, and cloud systems. It underpins:</p>

<ul>
  <li>Google Identity Platform, Microsoft Entra ID (Azure AD), Apple Sign In</li>
  <li>Enterprise SSO (Okta, Auth0, Ping Identity, Keycloak)</li>
  <li>Kubernetes service account tokens (projected volumes)</li>
  <li>GitHub Actions OIDC tokens for cloud federation</li>
  <li>AWS IAM Identity Center, GCP Workload Identity Federation</li>
</ul>

<hr />

<h3 id="2-oidc-architecture-and-components">2. OIDC Architecture and Components</h3>

<h4 id="21-identity-provider-idp-vs-authorization-server">2.1 Identity Provider (IdP) vs Authorization Server</h4>

<p>In pure OAuth 2.0, the <strong>Authorization Server (AS)</strong> issues access tokens and manages consent. In OIDC, this same server takes on an additional role: <strong>Identity Provider (IdP)</strong>.</p>

<p>The conceptual difference:</p>

<table>
  <thead>
    <tr>
      <th>Role</th>
      <th>Function</th>
      <th>Output</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Authorization Server</strong></td>
      <td>Authorizes client access to resources</td>
      <td>Access Token, Refresh Token</td>
    </tr>
    <tr>
      <td><strong>Identity Provider</strong></td>
      <td>Asserts the identity of the authenticated user</td>
      <td>ID Token, UserInfo claims</td>
    </tr>
  </tbody>
</table>

<p>In most deployments, a single server plays both roles simultaneously. The distinction matters architecturally because:</p>

<ul>
  <li>An AS can exist without being an IdP (pure OAuth 2.0 deployment)</li>
  <li>An IdP <em>must</em> implement OAuth 2.0 as its transport layer (per OIDC spec)</li>
  <li>Some enterprise architectures federate a separate IdP (e.g., an on-premises Active Directory Federation Services) into a cloud AS (e.g., Azure AD), creating a chain</li>
</ul>

<h4 id="22-relying-party-rp-vs-oauth-client">2.2 Relying Party (RP) vs OAuth Client</h4>

<p>In OIDC, the OAuth <strong>Client</strong> is referred to as the <strong>Relying Party (RP)</strong>. The terminological shift is intentional: the RP <em>relies</em> on the IdP’s assertion of identity.</p>

<p>Behavioral differences from a pure OAuth client:</p>

<ul>
  <li>The RP <strong>must</strong> validate the ID Token — this is non-optional and precisely specified in the spec</li>
  <li>The RP tracks session state and may initiate <strong>logout</strong> via the RP-initiated logout protocol</li>
  <li>The RP must handle the <code class="language-plaintext highlighter-rouge">nonce</code> parameter: generate it, bind it to the session, and verify it in the ID Token</li>
  <li>The RP is registered with the IdP with additional metadata beyond standard OAuth client registration (e.g., <code class="language-plaintext highlighter-rouge">id_token_signed_response_alg</code>, <code class="language-plaintext highlighter-rouge">userinfo_encrypted_response_alg</code>)</li>
</ul>

<h4 id="23-userinfo-endpoint-vs-id-token">2.3 UserInfo Endpoint vs ID Token</h4>

<p>OIDC provides two ways to receive identity claims about the user. They serve different purposes:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>ID Token</th>
      <th>UserInfo Endpoint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Format</strong></td>
      <td>Signed JWT</td>
      <td>JSON (optionally signed/encrypted)</td>
    </tr>
    <tr>
      <td><strong>Delivery</strong></td>
      <td>In the token response</td>
      <td>Via separate HTTP GET request</td>
    </tr>
    <tr>
      <td><strong>Bound to</strong></td>
      <td>Authentication event</td>
      <td>Current user (uses access token)</td>
    </tr>
    <tr>
      <td><strong>Purpose</strong></td>
      <td>Prove who authenticated</td>
      <td>Fetch additional profile data</td>
    </tr>
    <tr>
      <td><strong>Freshness</strong></td>
      <td>Snapshot at auth time</td>
      <td>May reflect current profile state</td>
    </tr>
    <tr>
      <td><strong>Latency</strong></td>
      <td>Zero (already in response)</td>
      <td>Requires extra network round-trip</td>
    </tr>
  </tbody>
</table>

<p><strong>When to use which:</strong></p>

<ul>
  <li>Use the <strong>ID Token</strong> for authentication decisions (who logged in, when, how)</li>
  <li>Use the <strong>UserInfo endpoint</strong> for profile data you need but that doesn’t need to be embedded in every token (avoids bloating tokens)</li>
  <li>Never use UserInfo data for making authentication decisions — it’s not cryptographically bound to the authentication event the same way the ID Token is</li>
</ul>

<h4 id="24-trust-model-and-metadata-discovery">2.4 Trust Model and Metadata Discovery</h4>

<p>The trust relationship in OIDC flows through:</p>

<ol>
  <li>
    <p><strong>Client Registration</strong> — The RP registers with the IdP (statically or dynamically via RFC 7591). The IdP issues <code class="language-plaintext highlighter-rouge">client_id</code> and optionally <code class="language-plaintext highlighter-rouge">client_secret</code>. Redirect URIs, allowed scopes, and token signing algorithms are registered here.</p>
  </li>
  <li>
    <p><strong>Discovery</strong> — The RP retrieves the IdP’s metadata from <code class="language-plaintext highlighter-rouge">/.well-known/openid-configuration</code> (covered in Section 5). This document anchors all subsequent trust decisions.</p>
  </li>
  <li>
    <p><strong>JWKS</strong> — The RP fetches the IdP’s public signing keys from the <code class="language-plaintext highlighter-rouge">jwks_uri</code> in the discovery document. All ID Token signatures are verified against these keys.</p>
  </li>
  <li>
    <p><strong>Issuer Binding</strong> — The <code class="language-plaintext highlighter-rouge">iss</code> claim in the ID Token must match the issuer in the discovery document. This is the root of the trust chain.</p>
  </li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RP trusts IdP's discovery document
  → retrieves signing keys from jwks_uri
  → verifies ID Token signature with those keys
  → validates iss matches the trusted issuer
  → trusts the identity claims in the token
</code></pre></div></div>

<hr />

<h3 id="3-id-token-deep-dive">3. ID Token Deep Dive</h3>

<p>The ID Token is the defining artifact of OIDC. It is a <strong>JWS (JSON Web Signature)</strong> — a signed JWT — that asserts the identity of the authenticated user.</p>

<h4 id="31-jwt-structure">3.1 JWT Structure</h4>

<p>A JWT has three components, each Base64URL-encoded and concatenated with <code class="language-plaintext highlighter-rouge">.</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BASE64URL(header) . BASE64URL(payload) . BASE64URL(signature)
</code></pre></div></div>

<h5 id="header">Header</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RS256"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"typ"</span><span class="p">:</span><span class="w"> </span><span class="s2">"JWT"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"kid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"signing-key-2024-01"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">alg</code></td>
      <td>Signing algorithm. Must be <code class="language-plaintext highlighter-rouge">RS256</code> or <code class="language-plaintext highlighter-rouge">ES256</code> in production. <strong>Never <code class="language-plaintext highlighter-rouge">none</code>.</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">typ</code></td>
      <td>Token type. <code class="language-plaintext highlighter-rouge">JWT</code> for standard tokens.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">kid</code></td>
      <td>Key ID. Used to look up the specific key in the JWKS endpoint to verify the signature.</td>
    </tr>
  </tbody>
</table>

<h5 id="payload-claims">Payload (Claims)</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"iss"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user-a1b2c3d4"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"aud"</span><span class="p">:</span><span class="w"> </span><span class="s2">"myapp-client-id"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1700003600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"iat"</span><span class="p">:</span><span class="w"> </span><span class="mi">1700000000</span><span class="p">,</span><span class="w">
  </span><span class="nl">"auth_time"</span><span class="p">:</span><span class="w"> </span><span class="mi">1699999800</span><span class="p">,</span><span class="w">
  </span><span class="nl">"nonce"</span><span class="p">:</span><span class="w"> </span><span class="s2">"n-0S6_WzA2Mj"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"azp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"myapp-client-id"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"at_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MTIzNDU2Nzg5MDEyMzQ"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"c_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LDktKdoQak3Pk0cnXxCltA"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"acr"</span><span class="p">:</span><span class="w"> </span><span class="s2">"urn:mace:incommon:iap:silver"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"amr"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"pwd"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mfa"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shaheryar Ahmed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shaheryar@example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email_verified"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h5 id="signature">Signature</h5>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RSA_SHA256(
  base64url(header) + "." + base64url(payload),
  IdP_private_key
)
</code></pre></div></div>

<p>The signature is verified by the RP using the IdP’s <strong>public key</strong> retrieved from the JWKS endpoint.</p>

<hr />

<h4 id="32-required-and-optional-claims">3.2 Required and Optional Claims</h4>

<h5 id="required-claims-must-be-present-in-every-id-token">Required Claims (MUST be present in every ID Token)</h5>

<table>
  <thead>
    <tr>
      <th>Claim</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">iss</code></td>
      <td>String (URL)</td>
      <td>Issuer identifier. Exact URL of the IdP. Must be HTTPS. Must match <code class="language-plaintext highlighter-rouge">issuer</code> in discovery document.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sub</code></td>
      <td>String</td>
      <td>Subject identifier. A stable, unique, opaque identifier for the user <strong>within this issuer</strong>. Not an email — never changes even if the user changes their email. Maximum 255 ASCII characters.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">aud</code></td>
      <td>String or Array</td>
      <td>Audience. Must include the <code class="language-plaintext highlighter-rouge">client_id</code> of the RP. If an array, all entries must be trusted.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exp</code></td>
      <td>NumericDate</td>
      <td>Expiry. Unix timestamp. The RP must reject the token if current time ≥ <code class="language-plaintext highlighter-rouge">exp</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">iat</code></td>
      <td>NumericDate</td>
      <td>Issued At. Unix timestamp of when the token was issued.</td>
    </tr>
  </tbody>
</table>

<h5 id="conditionally-required-claims">Conditionally Required Claims</h5>

<table>
  <thead>
    <tr>
      <th>Claim</th>
      <th>Required When</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">auth_time</code></td>
      <td><code class="language-plaintext highlighter-rouge">max_age</code> was requested, or IdP includes it</td>
      <td>Unix timestamp of when the user actually authenticated. Different from <code class="language-plaintext highlighter-rouge">iat</code> — token issuance can lag authentication.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nonce</code></td>
      <td><code class="language-plaintext highlighter-rouge">nonce</code> was sent in the auth request</td>
      <td>Echo of the <code class="language-plaintext highlighter-rouge">nonce</code> value. Used to bind the ID Token to a specific authentication request.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">acr</code></td>
      <td><code class="language-plaintext highlighter-rouge">acr_values</code> was requested</td>
      <td>Authentication Context Class Reference. Indicates the strength of authentication (e.g., MFA level).</td>
    </tr>
  </tbody>
</table>

<h5 id="optional-but-commonly-used-claims">Optional but Commonly Used Claims</h5>

<table>
  <thead>
    <tr>
      <th>Claim</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">azp</code></td>
      <td>Authorized Party. The <code class="language-plaintext highlighter-rouge">client_id</code> of the party the token was issued <em>for</em> — relevant when <code class="language-plaintext highlighter-rouge">aud</code> contains multiple values.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">at_hash</code></td>
      <td>Access Token Hash. Left half of SHA-256 of the access token, Base64URL-encoded. Cryptographically binds the ID Token to the access token.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">c_hash</code></td>
      <td>Code Hash. Left half of SHA-256 of the authorization code. Cryptographically binds the ID Token to the authorization code. Required in Hybrid Flow.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">amr</code></td>
      <td>Authentication Methods References. Array of strings indicating how authentication was performed (e.g., <code class="language-plaintext highlighter-rouge">["pwd", "otp"]</code>).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sid</code></td>
      <td>Session ID. Identifier for the authentication session. Used in logout flows.</td>
    </tr>
  </tbody>
</table>

<h5 id="standard-profile-claims-from-profile-scope">Standard Profile Claims (from <code class="language-plaintext highlighter-rouge">profile</code> scope)</h5>

<p><code class="language-plaintext highlighter-rouge">name</code>, <code class="language-plaintext highlighter-rouge">given_name</code>, <code class="language-plaintext highlighter-rouge">family_name</code>, <code class="language-plaintext highlighter-rouge">middle_name</code>, <code class="language-plaintext highlighter-rouge">nickname</code>, <code class="language-plaintext highlighter-rouge">preferred_username</code>, <code class="language-plaintext highlighter-rouge">profile</code>, <code class="language-plaintext highlighter-rouge">picture</code>, <code class="language-plaintext highlighter-rouge">website</code>, <code class="language-plaintext highlighter-rouge">gender</code>, <code class="language-plaintext highlighter-rouge">birthdate</code>, <code class="language-plaintext highlighter-rouge">zoneinfo</code>, <code class="language-plaintext highlighter-rouge">locale</code>, <code class="language-plaintext highlighter-rouge">updated_at</code></p>

<h5 id="standard-email-claims-from-email-scope">Standard Email Claims (from <code class="language-plaintext highlighter-rouge">email</code> scope)</h5>

<p><code class="language-plaintext highlighter-rouge">email</code>, <code class="language-plaintext highlighter-rouge">email_verified</code></p>

<h5 id="standard-phone-claims-from-phone-scope">Standard Phone Claims (from <code class="language-plaintext highlighter-rouge">phone</code> scope)</h5>

<p><code class="language-plaintext highlighter-rouge">phone_number</code>, <code class="language-plaintext highlighter-rouge">phone_number_verified</code></p>

<hr />

<h4 id="33-token-signing-jws-and-optional-encryption-jwe">3.3 Token Signing (JWS) and Optional Encryption (JWE)</h4>

<h5 id="jws-json-web-signature--the-default">JWS (JSON Web Signature) — The Default</h5>

<p>All ID Tokens MUST be signed. Supported algorithms:</p>

<table>
  <thead>
    <tr>
      <th>Algorithm</th>
      <th>Type</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RS256</code></td>
      <td>RSA + SHA-256</td>
      <td>Most common. Asymmetric — IdP signs with private key, RP verifies with public key.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ES256</code></td>
      <td>ECDSA + SHA-256</td>
      <td>More efficient than RSA. Increasingly preferred.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HS256</code></td>
      <td>HMAC + SHA-256</td>
      <td>Symmetric — requires the RP and IdP to share a secret. <strong>Avoid in most deployments.</strong> Vulnerable to key confusion attacks (Section 9).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">none</code></td>
      <td>None</td>
      <td>No signature. <strong>MUST NEVER be accepted in production.</strong></td>
    </tr>
  </tbody>
</table>

<p>The RP must be configured to accept only specific <code class="language-plaintext highlighter-rouge">alg</code> values. Accepting <code class="language-plaintext highlighter-rouge">alg</code> from the token header without validation is a critical vulnerability.</p>

<h5 id="jwe-json-web-encryption--optional">JWE (JSON Web Encryption) — Optional</h5>

<p>For sensitive deployments, the ID Token can additionally be <strong>encrypted</strong> (producing a JWE). The encryption wraps the entire JWS:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>JWE(JWS(payload)) → encrypted ID Token
</code></pre></div></div>

<p>The RP registers its public encryption key with the IdP. The IdP encrypts the JWS using the RP’s public key. Only the RP can decrypt it with its private key.</p>

<p>Relevant fields when encryption is used:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">id_token_encrypted_response_alg</code> — key agreement algorithm (e.g., <code class="language-plaintext highlighter-rouge">RSA-OAEP</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">id_token_encrypted_response_enc</code> — content encryption algorithm (e.g., <code class="language-plaintext highlighter-rouge">A256GCM</code>)</li>
</ul>

<p>JWE is rare in practice but important for high-assurance or regulated environments where token contents must not be visible to intermediaries.</p>

<hr />

<h4 id="34-key-rotation-and-jwks-endpoint">3.4 Key Rotation and JWKS Endpoint</h4>

<p>The IdP regularly rotates its signing keys. The JWKS (JSON Web Key Set) endpoint exposes all current public keys:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/jwks</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"keys"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"kty"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RSA"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"use"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sig"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"kid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"signing-key-2024-01"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RS256"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"n"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGyw..."</span><span class="p">,</span><span class="w">
      </span><span class="nl">"e"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AQAB"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"kty"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RSA"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"use"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sig"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"kid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"signing-key-2024-02"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RS256"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"n"</span><span class="p">:</span><span class="w"> </span><span class="s2">"new-key-modulus..."</span><span class="p">,</span><span class="w">
      </span><span class="nl">"e"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AQAB"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Key rotation process:</strong></p>

<ol>
  <li>IdP generates a new key pair, publishes new public key in JWKS (alongside old key)</li>
  <li>IdP begins signing new tokens with the new private key</li>
  <li>Old tokens (signed with old key) remain verifiable using the old public key still in JWKS</li>
  <li>After old tokens expire, IdP removes old public key from JWKS</li>
</ol>

<p><strong>RP caching strategy:</strong></p>

<ul>
  <li>Cache JWKS with a reasonable TTL (e.g., 1 hour)</li>
  <li>If signature verification fails with cached keys, <strong>re-fetch JWKS once</strong> and retry</li>
  <li>Do not re-fetch on every token validation (DDoS risk to IdP)</li>
  <li>Use <code class="language-plaintext highlighter-rouge">kid</code> from the token header to select the correct key — avoid trying all keys</li>
</ul>

<hr />

<h4 id="35-sample-id-token--full-annotated-example">3.5 Sample ID Token — Full Annotated Example</h4>

<p><strong>Encoded:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ25pbmcta2V5LTIwMjQtMDEifQ
.
eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLWExYjJjM2Q0IiwiYXVkIjoibXlhcHAtY2xpZW50LWlkIiwiZXhwIjoxNzAwMDAzNjAwLCJpYXQiOjE3MDAwMDAwMDAsImF1dGhfdGltZSI6MTY5OTk5OTgwMCwibm9uY2UiOiJuLTBTNl9XekEyTWoiLCJhenAiOiJteWFwcC1jbGllbnQtaWQiLCJhdF9oYXNoIjoiTVRJek5EVTJOemM0T1RBeE1qTTAiLCJhY3IiOiJ1cm46bWFjZTppbmNvbW1vbjppYXA6c2lsdmVyIiwiYW1yIjpbInB3ZCIsIm1mYSJdLCJuYW1lIjoiU2hhaGVyeWFyIEFobWVkIiwiZW1haWwiOiJzaGFoZXJ5YXJAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0
.
SIGNATURE
</code></pre></div></div>

<p><strong>Decoded Payload:</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"iss"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com"</span><span class="p">,</span><span class="w">      </span><span class="err">//</span><span class="w"> </span><span class="err">Issuer</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="err">must</span><span class="w"> </span><span class="err">match</span><span class="w"> </span><span class="err">discovery</span><span class="w"> </span><span class="err">doc</span><span class="w">
  </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user-a1b2c3d4"</span><span class="p">,</span><span class="w">                 </span><span class="err">//</span><span class="w"> </span><span class="err">Stable</span><span class="p">,</span><span class="w"> </span><span class="err">opaque</span><span class="w"> </span><span class="err">user</span><span class="w"> </span><span class="err">ID</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="err">use</span><span class="w"> </span><span class="err">this</span><span class="p">,</span><span class="w"> </span><span class="err">not</span><span class="w"> </span><span class="err">email</span><span class="w">
  </span><span class="nl">"aud"</span><span class="p">:</span><span class="w"> </span><span class="s2">"myapp-client-id"</span><span class="p">,</span><span class="w">               </span><span class="err">//</span><span class="w"> </span><span class="err">Audience</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="err">must</span><span class="w"> </span><span class="err">match</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">RP's</span><span class="w"> </span><span class="err">client_id</span><span class="w">
  </span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1700003600</span><span class="p">,</span><span class="w">                      </span><span class="err">//</span><span class="w"> </span><span class="err">Expires:</span><span class="w"> </span><span class="mi">2023-11-14</span><span class="err">T</span><span class="mi">22</span><span class="err">:</span><span class="mi">13</span><span class="err">:</span><span class="mi">20</span><span class="err">Z</span><span class="w">
  </span><span class="nl">"iat"</span><span class="p">:</span><span class="w"> </span><span class="mi">1700000000</span><span class="p">,</span><span class="w">                      </span><span class="err">//</span><span class="w"> </span><span class="err">Issued:</span><span class="w"> </span><span class="mi">2023-11-14</span><span class="err">T</span><span class="mi">21</span><span class="err">:</span><span class="mi">13</span><span class="err">:</span><span class="mi">20</span><span class="err">Z</span><span class="w"> </span><span class="err">(</span><span class="mi">1</span><span class="w"> </span><span class="err">hour</span><span class="w"> </span><span class="err">token)</span><span class="w">
  </span><span class="nl">"auth_time"</span><span class="p">:</span><span class="w"> </span><span class="mi">1699999800</span><span class="p">,</span><span class="w">                </span><span class="err">//</span><span class="w"> </span><span class="err">User</span><span class="w"> </span><span class="err">actually</span><span class="w"> </span><span class="err">authenticated</span><span class="w"> </span><span class="mi">200</span><span class="err">s</span><span class="w"> </span><span class="err">before</span><span class="w"> </span><span class="err">issuance</span><span class="w">
  </span><span class="nl">"nonce"</span><span class="p">:</span><span class="w"> </span><span class="s2">"n-0S6_WzA2Mj"</span><span class="p">,</span><span class="w">               </span><span class="err">//</span><span class="w"> </span><span class="err">Must</span><span class="w"> </span><span class="err">match</span><span class="w"> </span><span class="err">nonce</span><span class="w"> </span><span class="err">sent</span><span class="w"> </span><span class="err">in</span><span class="w"> </span><span class="err">auth</span><span class="w"> </span><span class="err">request</span><span class="w">
  </span><span class="nl">"azp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"myapp-client-id"</span><span class="p">,</span><span class="w">              </span><span class="err">//</span><span class="w"> </span><span class="err">Authorized</span><span class="w"> </span><span class="err">party</span><span class="w"> </span><span class="err">(same</span><span class="w"> </span><span class="err">as</span><span class="w"> </span><span class="err">aud</span><span class="w"> </span><span class="err">here)</span><span class="w">
  </span><span class="nl">"at_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MTIzNDU2Nzg5MDEyMzQ"</span><span class="p">,</span><span class="w">     </span><span class="err">//</span><span class="w"> </span><span class="err">Binds</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">ID</span><span class="w"> </span><span class="err">Token</span><span class="w"> </span><span class="err">to</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">access</span><span class="w"> </span><span class="err">token</span><span class="w">
  </span><span class="nl">"acr"</span><span class="p">:</span><span class="w"> </span><span class="s2">"urn:mace:incommon:iap:silver"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">Authentication</span><span class="w"> </span><span class="err">assurance</span><span class="w"> </span><span class="err">level</span><span class="w">
  </span><span class="nl">"amr"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"pwd"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mfa"</span><span class="p">],</span><span class="w">                  </span><span class="err">//</span><span class="w"> </span><span class="err">Authenticated</span><span class="w"> </span><span class="err">via</span><span class="w"> </span><span class="err">password</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="err">MFA</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shaheryar Ahmed"</span><span class="p">,</span><span class="w">              </span><span class="err">//</span><span class="w"> </span><span class="err">From</span><span class="w"> </span><span class="err">profile</span><span class="w"> </span><span class="err">scope</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shaheryar@example.com"</span><span class="p">,</span><span class="w">       </span><span class="err">//</span><span class="w"> </span><span class="err">From</span><span class="w"> </span><span class="err">email</span><span class="w"> </span><span class="err">scope</span><span class="w">
  </span><span class="nl">"email_verified"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">                  </span><span class="err">//</span><span class="w"> </span><span class="err">IdP</span><span class="w"> </span><span class="err">has</span><span class="w"> </span><span class="err">confirmed</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">email</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h3 id="4-oidc-flow-internals">4. OIDC Flow Internals</h3>

<p>OIDC uses the same underlying flows as OAuth 2.0 but modifies their behavior. The <code class="language-plaintext highlighter-rouge">openid</code> scope is what triggers OIDC behavior at the Authorization Server.</p>

<hr />

<h4 id="41-authorization-code-flow-oidc-context">4.1 Authorization Code Flow (OIDC Context)</h4>

<p>This is the standard OIDC flow. Identical to OAuth 2.0 Authorization Code Flow with OIDC-specific additions.</p>

<p><strong>What changes compared to pure OAuth 2.0:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">openid</code> is required in <code class="language-plaintext highlighter-rouge">scope</code></li>
  <li><code class="language-plaintext highlighter-rouge">nonce</code> should be sent (required if <code class="language-plaintext highlighter-rouge">response_type</code> includes <code class="language-plaintext highlighter-rouge">id_token</code>)</li>
  <li>Token endpoint returns an <code class="language-plaintext highlighter-rouge">id_token</code> in addition to <code class="language-plaintext highlighter-rouge">access_token</code></li>
  <li>The RP must validate the ID Token before trusting any identity claims</li>
</ul>

<h5 id="step-1-authorization-request">Step 1: Authorization Request</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?
  response_type=code
  &amp;client_id=myapp-client-id
  &amp;redirect_uri=https://myapp.com/callback
  &amp;scope=openid%20profile%20email
  &amp;state=xK9mP2qR7vL4nJ1w
  &amp;nonce=n-0S6_WzA2Mj
  &amp;prompt=login
  &amp;max_age=3600
HTTP/1.1
Host: auth.example.com
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">nonce</code> value must be:</p>

<ul>
  <li>Cryptographically random (≥128 bits of entropy)</li>
  <li>Stored server-side or in a secure, HttpOnly session cookie</li>
  <li>Verified against the <code class="language-plaintext highlighter-rouge">nonce</code> claim in the returned ID Token</li>
</ul>

<h5 id="step-2-token-exchange">Step 2: Token Exchange</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Basic base64(myapp-client-id:client_secret)</span>

grant_type=authorization_code
&amp;code=SplxlOBeZQQYbYS6WxSbIA
&amp;redirect_uri=https://myapp.com/callback
</code></pre></div></div>

<h5 id="step-3-token-response-oidc">Step 3: Token Response (OIDC)</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiJ9.ACCESS..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8xLOxBtZp8"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZ25pbmcta2V5LTIwMjQtMDEifQ.eyJpc3MiOi..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"openid profile email"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">id_token</code> is exclusively in the token endpoint response. It is <strong>never</strong> sent to the Resource Server.</p>

<h5 id="id-token-validation-step-4--critical">ID Token Validation (Step 4 — Critical)</h5>

<p>Full validation process covered in Section 7. At minimum:</p>

<ol>
  <li>Verify the signature</li>
  <li>Verify <code class="language-plaintext highlighter-rouge">iss</code>, <code class="language-plaintext highlighter-rouge">aud</code>, <code class="language-plaintext highlighter-rouge">exp</code>, <code class="language-plaintext highlighter-rouge">iat</code></li>
  <li>Verify <code class="language-plaintext highlighter-rouge">nonce</code> matches what was sent</li>
  <li>If <code class="language-plaintext highlighter-rouge">at_hash</code> is present, verify it against the access token</li>
</ol>

<hr />

<h4 id="42-authorization-code--pkce-recommended-modern-flow">4.2 Authorization Code + PKCE (Recommended Modern Flow)</h4>

<p>PKCE and OIDC are independent extensions to OAuth 2.0. They compose seamlessly and should always be used together for public clients.</p>

<h5 id="how-they-interact">How They Interact</h5>

<p>PKCE protects the <strong>authorization code</strong> from being stolen and exchanged by an attacker. OIDC adds the <strong>ID Token</strong> to the token response. Together:</p>

<ul>
  <li>PKCE prevents code interception → attacker can’t get tokens at all</li>
  <li>OIDC <code class="language-plaintext highlighter-rouge">nonce</code> prevents replay → even if an old ID Token is captured, it can’t be replayed into a new session</li>
  <li><code class="language-plaintext highlighter-rouge">at_hash</code> in the ID Token cryptographically binds the ID Token to the specific access token issued — prevents token substitution</li>
</ul>

<h5 id="request-combined-pkce--oidc">Request (Combined PKCE + OIDC)</h5>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?
  response_type=code
  &amp;client_id=myapp-spa
  &amp;redirect_uri=https://myapp.com/callback
  &amp;scope=openid%20profile%20email
  &amp;state=xK9mP2qR7vL4nJ1w
  &amp;nonce=n-0S6_WzA2Mj
  &amp;code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &amp;code_challenge_method=S256
HTTP/1.1
Host: auth.example.com
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">nonce</code> serves a different purpose than <code class="language-plaintext highlighter-rouge">state</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">state</code> → CSRF protection for the OAuth redirect</li>
  <li><code class="language-plaintext highlighter-rouge">nonce</code> → replay protection for the ID Token itself</li>
</ul>

<p>Both must be present and validated.</p>

<hr />

<h4 id="43-hybrid-flow">4.3 Hybrid Flow</h4>

<p>The Hybrid Flow allows the RP to receive some tokens <strong>from the authorization endpoint</strong> (via redirect) and other tokens <strong>from the token endpoint</strong> (via back-channel). It is defined by using specific <code class="language-plaintext highlighter-rouge">response_type</code> combinations.</p>

<h5 id="response_type-values"><code class="language-plaintext highlighter-rouge">response_type</code> Values</h5>

<table>
  <thead>
    <tr>
      <th><code class="language-plaintext highlighter-rouge">response_type</code></th>
      <th>From Auth Endpoint</th>
      <th>From Token Endpoint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code</code></td>
      <td>code</td>
      <td>access_token, id_token, refresh_token</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">id_token</code></td>
      <td>id_token</td>
      <td>—</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code id_token</code></td>
      <td>code + id_token</td>
      <td>access_token, id_token, refresh_token</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code token</code></td>
      <td>code + access_token</td>
      <td>id_token, refresh_token</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code id_token token</code></td>
      <td>code + id_token + access_token</td>
      <td>id_token, refresh_token</td>
    </tr>
  </tbody>
</table>

<p>The most common Hybrid Flow uses <code class="language-plaintext highlighter-rouge">code id_token</code>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?
  response_type=code%20id_token
  &amp;client_id=myapp-client-id
  &amp;redirect_uri=https://myapp.com/callback
  &amp;scope=openid%20profile
  &amp;state=xK9mP2qR7vL4nJ1w
  &amp;nonce=n-0S6_WzA2Mj
HTTP/1.1
Host: auth.example.com
</span></code></pre></div></div>

<p><strong>Response (via fragment):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://myapp.com/callback#
  code=SplxlOBeZQQYbYS6WxSbIA
  &amp;id_token=eyJhbGciOiJSUzI1NiJ9...
  &amp;state=xK9mP2qR7vL4nJ1w
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">id_token</code> returned from the authorization endpoint contains a <code class="language-plaintext highlighter-rouge">c_hash</code> claim:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"c_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LDktKdoQak3Pk0cnXxCltA"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">c_hash</code> = Base64URL(left_half(SHA256(authorization_code)))</p>

<p>The RP verifies <code class="language-plaintext highlighter-rouge">c_hash</code> against the received <code class="language-plaintext highlighter-rouge">code</code> before using either. This binds the ID Token to the code, preventing substitution attacks.</p>

<h5 id="security-implications-of-hybrid-flow">Security Implications of Hybrid Flow</h5>

<ul>
  <li>Tokens returned in the authorization endpoint response travel via browser redirect (URL fragment or query string) — higher exposure risk</li>
  <li>The ID Token from the auth endpoint typically has fewer claims (it’s a “hint”) — the full ID Token with all claims comes from the token endpoint</li>
  <li><code class="language-plaintext highlighter-rouge">c_hash</code> and <code class="language-plaintext highlighter-rouge">at_hash</code> are the critical security bindings — if validation is skipped, token substitution becomes possible</li>
  <li><strong>Modern recommendation:</strong> Avoid Hybrid Flow unless there is a specific architectural reason. Authorization Code + PKCE achieves the same security goals with less complexity.</li>
</ul>

<hr />

<h4 id="44-implicit-flow-historical">4.4 Implicit Flow (Historical)</h4>

<p>In OIDC Implicit Flow, the ID Token (and optionally access token) were returned directly from the authorization endpoint via URL fragment:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://myapp.com/callback#
  id_token=eyJhbGciOiJSUzI1NiJ9...
  &amp;access_token=SlAV32hkKG
  &amp;token_type=Bearer
  &amp;expires_in=3600
  &amp;state=xK9mP2qR7vL4nJ1w
</code></pre></div></div>

<p><strong>Why it existed:</strong> Before PKCE, public clients had no secure way to use the Authorization Code Flow (no secret to authenticate the token exchange). The Implicit Flow removed the token exchange step entirely.</p>

<p><strong>Why it is deprecated:</strong></p>

<ul>
  <li>ID Token in URL fragment → appears in browser history, server logs, proxy logs, <code class="language-plaintext highlighter-rouge">Referer</code> headers</li>
  <li>No refresh token support</li>
  <li>No <code class="language-plaintext highlighter-rouge">c_hash</code> or <code class="language-plaintext highlighter-rouge">at_hash</code> validation possible in the auth endpoint response for <code class="language-plaintext highlighter-rouge">response_type=id_token</code></li>
  <li>PKCE makes it entirely unnecessary</li>
</ul>

<p><strong>What replaced it:</strong> Authorization Code + PKCE for all public clients.</p>

<hr />

<h3 id="5-protocol-endpoints-and-discovery">5. Protocol Endpoints and Discovery</h3>

<h4 id="51-well-knownopenid-configuration">5.1 <code class="language-plaintext highlighter-rouge">/.well-known/openid-configuration</code></h4>

<p>The OIDC discovery document is a JSON document served at a well-known URL. It is the foundation of the trust model — the RP fetches this document to configure itself.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/.well-known/openid-configuration</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
</code></pre></div></div>

<h5 id="sample-discovery-document">Sample Discovery Document</h5>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"issuer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"authorization_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/authorize"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/token"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"userinfo_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/userinfo"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"jwks_uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/jwks"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"registration_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/register"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"end_session_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/logout"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"revocation_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/revoke"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"introspection_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/introspect"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"device_authorization_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/device_authorization"</span><span class="p">,</span><span class="w">

  </span><span class="nl">"scopes_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"openid"</span><span class="p">,</span><span class="w"> </span><span class="s2">"profile"</span><span class="p">,</span><span class="w"> </span><span class="s2">"email"</span><span class="p">,</span><span class="w"> </span><span class="s2">"phone"</span><span class="p">,</span><span class="w"> </span><span class="s2">"offline_access"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"response_types_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"code"</span><span class="p">,</span><span class="w"> </span><span class="s2">"id_token"</span><span class="p">,</span><span class="w"> </span><span class="s2">"code id_token"</span><span class="p">,</span><span class="w"> </span><span class="s2">"code token"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"response_modes_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"query"</span><span class="p">,</span><span class="w"> </span><span class="s2">"fragment"</span><span class="p">,</span><span class="w"> </span><span class="s2">"form_post"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"grant_types_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"authorization_code"</span><span class="p">,</span><span class="w"> </span><span class="s2">"refresh_token"</span><span class="p">,</span><span class="w"> </span><span class="s2">"client_credentials"</span><span class="p">],</span><span class="w">

  </span><span class="nl">"id_token_signing_alg_values_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"RS256"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ES256"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"id_token_encryption_alg_values_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"RSA-OAEP"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"id_token_encryption_enc_values_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"A256GCM"</span><span class="p">],</span><span class="w">

  </span><span class="nl">"userinfo_signing_alg_values_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"RS256"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"token_endpoint_auth_methods_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"client_secret_basic"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"client_secret_post"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"private_key_jwt"</span><span class="w">
  </span><span class="p">],</span><span class="w">

  </span><span class="nl">"subject_types_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"public"</span><span class="p">,</span><span class="w"> </span><span class="s2">"pairwise"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"claim_types_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"normal"</span><span class="p">,</span><span class="w"> </span><span class="s2">"aggregated"</span><span class="p">,</span><span class="w"> </span><span class="s2">"distributed"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"claims_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"sub"</span><span class="p">,</span><span class="w"> </span><span class="s2">"iss"</span><span class="p">,</span><span class="w"> </span><span class="s2">"aud"</span><span class="p">,</span><span class="w"> </span><span class="s2">"exp"</span><span class="p">,</span><span class="w"> </span><span class="s2">"iat"</span><span class="p">,</span><span class="w"> </span><span class="s2">"auth_time"</span><span class="p">,</span><span class="w"> </span><span class="s2">"nonce"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"name"</span><span class="p">,</span><span class="w"> </span><span class="s2">"email"</span><span class="p">,</span><span class="w"> </span><span class="s2">"email_verified"</span><span class="p">,</span><span class="w"> </span><span class="s2">"phone_number"</span><span class="w">
  </span><span class="p">],</span><span class="w">

  </span><span class="nl">"code_challenge_methods_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"S256"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"request_parameter_supported"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"request_uri_parameter_supported"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"require_request_uri_registration"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">

  </span><span class="nl">"frontchannel_logout_supported"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"backchannel_logout_supported"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"backchannel_logout_session_supported"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h5 id="key-fields-explained">Key Fields Explained</h5>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Significance</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">issuer</code></td>
      <td>The canonical IdP URL. <strong>Must be HTTPS. Must match <code class="language-plaintext highlighter-rouge">iss</code> in all ID Tokens.</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">jwks_uri</code></td>
      <td>Where the RP fetches public signing keys.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">subject_types_supported</code></td>
      <td><code class="language-plaintext highlighter-rouge">public</code> = same <code class="language-plaintext highlighter-rouge">sub</code> for all RPs. <code class="language-plaintext highlighter-rouge">pairwise</code> = different <code class="language-plaintext highlighter-rouge">sub</code> per RP (privacy-preserving).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code_challenge_methods_supported</code></td>
      <td>If <code class="language-plaintext highlighter-rouge">S256</code> is listed, PKCE is supported. If this field is absent, assume no PKCE support.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">token_endpoint_auth_methods_supported</code></td>
      <td>How the client authenticates to the token endpoint. <code class="language-plaintext highlighter-rouge">private_key_jwt</code> is the most secure option.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">backchannel_logout_supported</code></td>
      <td>Whether the IdP supports OIDC Back-Channel Logout (server-to-server session termination).</td>
    </tr>
  </tbody>
</table>

<h4 id="52-dynamic-discovery-process">5.2 Dynamic Discovery Process</h4>

<ol>
  <li>RP is configured with the IdP’s <strong>issuer URL</strong> (e.g., <code class="language-plaintext highlighter-rouge">https://auth.example.com</code>)</li>
  <li>RP constructs the discovery URL: <code class="language-plaintext highlighter-rouge">{issuer}/.well-known/openid-configuration</code></li>
  <li>RP fetches and caches the document</li>
  <li>RP extracts endpoint URLs and supported capabilities</li>
  <li>RP configures itself (sets <code class="language-plaintext highlighter-rouge">authorization_endpoint</code>, <code class="language-plaintext highlighter-rouge">token_endpoint</code>, <code class="language-plaintext highlighter-rouge">jwks_uri</code>, etc.)</li>
</ol>

<p>This makes OIDC self-configuring — changing an IdP’s endpoint URLs only requires updating the discovery document, not reconfiguring every RP.</p>

<h4 id="53-jwks-endpoint">5.3 JWKS Endpoint</h4>

<p>Covered in Section 3.4. Key operational notes:</p>

<ul>
  <li>The JWKS response may contain multiple keys (different <code class="language-plaintext highlighter-rouge">kid</code> values, key rotation overlap)</li>
  <li>The RP selects the key matching the <code class="language-plaintext highlighter-rouge">kid</code> in the token header</li>
  <li>If no matching <code class="language-plaintext highlighter-rouge">kid</code> is found, refresh the JWKS and retry <strong>once</strong></li>
  <li>JWKS responses should be cached (respect <code class="language-plaintext highlighter-rouge">Cache-Control</code> headers, or default to ~1 hour TTL)</li>
</ul>

<h4 id="54-userinfo-endpoint">5.4 UserInfo Endpoint</h4>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/userinfo</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Bearer eyJhbGciOiJSUzI1NiJ9.ACCESS_TOKEN...</span>
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user-a1b2c3d4"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shaheryar Ahmed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"given_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shaheryar"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"family_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ahmed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shaheryar@example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email_verified"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"phone_number"</span><span class="p">:</span><span class="w"> </span><span class="s2">"+92-300-1234567"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"picture"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com/users/shaheryar/photo.jpg"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Critical:</strong> The <code class="language-plaintext highlighter-rouge">sub</code> in the UserInfo response MUST match the <code class="language-plaintext highlighter-rouge">sub</code> in the ID Token. If they don’t match, the RP must reject the response — this would indicate a response substitution attack.</p>

<p>The UserInfo response can also be returned as a <strong>signed JWT</strong> (if the IdP supports <code class="language-plaintext highlighter-rouge">userinfo_signed_response_alg</code>), providing authenticity guarantees beyond the TLS transport.</p>

<h4 id="55-token-endpoint-differences-in-oidc">5.5 Token Endpoint Differences in OIDC</h4>

<p>The token endpoint behaves identically to OAuth 2.0 with one addition: when <code class="language-plaintext highlighter-rouge">openid</code> is in scope, the response includes <code class="language-plaintext highlighter-rouge">id_token</code>.</p>

<p><strong>Client authentication methods at the token endpoint:</strong></p>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>How It Works</th>
      <th>Security Level</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">client_secret_basic</code></td>
      <td><code class="language-plaintext highlighter-rouge">Authorization: Basic base64(id:secret)</code></td>
      <td>Standard</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">client_secret_post</code></td>
      <td><code class="language-plaintext highlighter-rouge">client_secret</code> in POST body</td>
      <td>Weaker (body logging risk)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">client_secret_jwt</code></td>
      <td>JWT signed with the shared secret</td>
      <td>Stronger</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">private_key_jwt</code></td>
      <td>JWT signed with client’s private key</td>
      <td>Strongest — recommended for high-assurance</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">none</code></td>
      <td>No authentication (public clients)</td>
      <td>Must use PKCE</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">private_key_jwt</code> is the recommended method for confidential clients in high-security deployments. The client signs a JWT assertion with its private key; the IdP verifies with the registered public key. This eliminates the need to share any secret.</p>

<hr />

<h3 id="6-advanced-parameters-and-behavior">6. Advanced Parameters and Behavior</h3>

<h4 id="61-nonce">6.1 <code class="language-plaintext highlighter-rouge">nonce</code></h4>

<p>The <code class="language-plaintext highlighter-rouge">nonce</code> is a string value sent by the RP in the authorization request. The IdP embeds it verbatim in the ID Token as the <code class="language-plaintext highlighter-rouge">nonce</code> claim.</p>

<p><strong>Purpose:</strong> Binds the ID Token to a specific authentication request, preventing ID Token replay attacks.</p>

<p><strong>Lifecycle:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. RP generates: nonce = cryptographically_random_string()
2. RP stores: session["nonce"] = nonce  (server-side, never in URL)
3. RP includes in auth request: &amp;nonce=&lt;value&gt;
4. IdP embeds in ID Token: { "nonce": "&lt;value&gt;" }
5. RP validates: assert id_token_claims["nonce"] == session["nonce"]
6. RP invalidates: delete session["nonce"]  (single-use)
</code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">nonce</code> that is reused, predictable, or not validated defeats replay protection. The nonce must be:</p>

<ul>
  <li>Cryptographically random (min 128 bits)</li>
  <li>Single-use (invalidated after one verification)</li>
  <li>Stored in a server-side session or HttpOnly cookie (not in localStorage)</li>
</ul>

<h4 id="62-state">6.2 <code class="language-plaintext highlighter-rouge">state</code></h4>

<p>Inherited from OAuth 2.0. In OIDC context: still for CSRF protection of the authorization redirect. Distinct from <code class="language-plaintext highlighter-rouge">nonce</code> — they protect different things at different layers.</p>

<p><code class="language-plaintext highlighter-rouge">state</code> → protects the <strong>OAuth redirect</strong> (is this callback expected?)<br />
<code class="language-plaintext highlighter-rouge">nonce</code> → protects the <strong>ID Token</strong> (is this token for this session?)</p>

<p>Both must be present. Both must be validated.</p>

<h4 id="63-prompt">6.3 <code class="language-plaintext highlighter-rouge">prompt</code></h4>

<p>Controls whether the IdP forces the user to interact, regardless of existing session state.</p>

<table>
  <thead>
    <tr>
      <th>Value</th>
      <th>Behavior</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">none</code></td>
      <td>No UI interaction. If authentication or consent is required, return an error (<code class="language-plaintext highlighter-rouge">login_required</code>, <code class="language-plaintext highlighter-rouge">interaction_required</code>). Used for silent authentication checks.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">login</code></td>
      <td>Force re-authentication even if the user has an existing session. Useful for sensitive operations.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">consent</code></td>
      <td>Force re-display of the consent screen even if the user has previously consented.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">select_account</code></td>
      <td>Show an account selector even if only one session exists.</td>
    </tr>
  </tbody>
</table>

<p><strong><code class="language-plaintext highlighter-rouge">prompt=none</code> use case — silent token refresh in SPAs:</strong></p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?
  response_type=code
  &amp;client_id=myapp-spa
  &amp;redirect_uri=https://myapp.com/silent-callback
  &amp;scope=openid
  &amp;prompt=none
  &amp;nonce=new-nonce
  &amp;code_challenge=...
</span></code></pre></div></div>

<p>This is done in a hidden iframe. If the user has an active session at the IdP, a new ID Token is issued silently. If not, the error is caught and the user is redirected to login.</p>

<h4 id="64-max_age">6.4 <code class="language-plaintext highlighter-rouge">max_age</code></h4>

<p>Specifies the maximum elapsed time (in seconds) since the user’s last active authentication. If <code class="language-plaintext highlighter-rouge">auth_time</code> in the ID Token indicates the user authenticated longer ago than <code class="language-plaintext highlighter-rouge">max_age</code>, the IdP must re-authenticate the user.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?...&amp;max_age=900
</span></code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">max_age=900</code>, the IdP must ensure the user authenticated within the last 15 minutes. If not, it forces re-login before issuing the token.</p>

<p>The RP must also validate: <code class="language-plaintext highlighter-rouge">current_time - auth_time ≤ max_age</code> on the received ID Token.</p>

<h4 id="65-acr_values">6.5 <code class="language-plaintext highlighter-rouge">acr_values</code></h4>

<p>Authentication Context Class Reference. The RP requests a specific assurance level for authentication:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?...&amp;acr_values=urn:mace:incommon:iap:silver%20urn:mace:incommon:iap:bronze
</span></code></pre></div></div>

<p>(Space-separated list, ordered by preference.)</p>

<p>The IdP includes the actual <code class="language-plaintext highlighter-rouge">acr</code> achieved in the ID Token:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"acr"</span><span class="p">:</span><span class="w"> </span><span class="s2">"urn:mace:incommon:iap:silver"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Common ACR values:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">urn:mace:incommon:iap:bronze</code> — password only</li>
  <li><code class="language-plaintext highlighter-rouge">urn:mace:incommon:iap:silver</code> — password + second factor</li>
  <li><code class="language-plaintext highlighter-rouge">urn:mace:incommon:iap:gold</code> — hardware token or biometric</li>
</ul>

<p>The RP should verify the <code class="language-plaintext highlighter-rouge">acr</code> in the ID Token meets its minimum requirement for the requested operation.</p>

<h4 id="66-login_hint">6.6 <code class="language-plaintext highlighter-rouge">login_hint</code></h4>

<p>A hint to the IdP about which user is attempting to authenticate. Typically an email address or username. Allows the IdP to pre-populate the login form.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?...&amp;login_hint=user%40example.com
</span></code></pre></div></div>

<p>This is only a hint — the IdP may ignore it. The authenticated user’s identity is always taken from the <code class="language-plaintext highlighter-rouge">sub</code> in the ID Token, not from <code class="language-plaintext highlighter-rouge">login_hint</code>.</p>

<h4 id="67-response_type-and-response_mode">6.7 <code class="language-plaintext highlighter-rouge">response_type</code> and <code class="language-plaintext highlighter-rouge">response_mode</code></h4>

<p><code class="language-plaintext highlighter-rouge">response_type</code> defines which tokens are returned and from which endpoint (authorization or token). See Section 4.3 for the full table.</p>

<p><code class="language-plaintext highlighter-rouge">response_mode</code> defines <strong>how</strong> the authorization endpoint delivers its response to the RP:</p>

<table>
  <thead>
    <tr>
      <th>Value</th>
      <th>Delivery Method</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">query</code></td>
      <td>URL query string (<code class="language-plaintext highlighter-rouge">?code=...</code>)</td>
      <td>Default for <code class="language-plaintext highlighter-rouge">response_type=code</code>. Tokens visible in logs.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fragment</code></td>
      <td>URL fragment (<code class="language-plaintext highlighter-rouge">#code=...</code>)</td>
      <td>Default for implicit flow. Not sent to server.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">form_post</code></td>
      <td>HTTP POST with form data</td>
      <td>Most secure for front-channel. Response not in URL or logs.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">jwt</code> (JARM)</td>
      <td>Signed JWT containing all parameters</td>
      <td>JWT Authorization Response Mode — advanced, provides integrity</td>
    </tr>
  </tbody>
</table>

<p>For <code class="language-plaintext highlighter-rouge">response_type=code</code>, prefer <code class="language-plaintext highlighter-rouge">response_mode=form_post</code> in high-security deployments to prevent authorization codes from appearing in server logs.</p>

<h4 id="68-scope-semantics-in-oidc">6.8 Scope Semantics in OIDC</h4>

<table>
  <thead>
    <tr>
      <th>Scope</th>
      <th>Claims Returned</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">openid</code></td>
      <td><strong>Required.</strong> Triggers OIDC. Returns <code class="language-plaintext highlighter-rouge">sub</code> at minimum. Without this, it’s plain OAuth 2.0.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">profile</code></td>
      <td><code class="language-plaintext highlighter-rouge">name</code>, <code class="language-plaintext highlighter-rouge">given_name</code>, <code class="language-plaintext highlighter-rouge">family_name</code>, <code class="language-plaintext highlighter-rouge">nickname</code>, <code class="language-plaintext highlighter-rouge">picture</code>, <code class="language-plaintext highlighter-rouge">website</code>, <code class="language-plaintext highlighter-rouge">gender</code>, <code class="language-plaintext highlighter-rouge">birthdate</code>, <code class="language-plaintext highlighter-rouge">zoneinfo</code>, <code class="language-plaintext highlighter-rouge">locale</code>, <code class="language-plaintext highlighter-rouge">updated_at</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">email</code></td>
      <td><code class="language-plaintext highlighter-rouge">email</code>, <code class="language-plaintext highlighter-rouge">email_verified</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">phone</code></td>
      <td><code class="language-plaintext highlighter-rouge">phone_number</code>, <code class="language-plaintext highlighter-rouge">phone_number_verified</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">address</code></td>
      <td><code class="language-plaintext highlighter-rouge">address</code> (structured JSON object)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">offline_access</code></td>
      <td>Issues a <code class="language-plaintext highlighter-rouge">refresh_token</code>. Without this, no refresh token is issued in OIDC flows.</td>
    </tr>
  </tbody>
</table>

<p>Claims may be delivered in the ID Token directly, from the UserInfo endpoint, or both — depending on IdP configuration. RPs should request claims from UserInfo for large payloads (avoids bloating tokens stored in session state).</p>

<hr />

<h3 id="7-token-validation-and-trust-chain">7. Token Validation and Trust Chain</h3>

<h4 id="71-full-id-token-validation-process">7.1 Full ID Token Validation Process</h4>

<p>The RP MUST perform all of these checks. Skipping any is a security vulnerability.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Step 1: Decrypt (if encrypted)
  - If id_token is a JWE, decrypt using the RP's private key
  - The result is the underlying JWS

Step 2: Parse the JWT
  - Split by "."
  - Base64URL-decode header and payload (no key needed)
  - Do NOT trust claims until signature is verified

Step 3: Algorithm check
  - header["alg"] must be an algorithm the RP is configured to accept
  - REJECT if alg == "none"
  - REJECT if alg is not in the RP's allowlist

Step 4: Key selection
  - Use header["kid"] to select the key from the cached JWKS
  - If kid is not found, re-fetch JWKS once and retry
  - If still not found, REJECT

Step 5: Signature verification
  - Verify the JWT signature using the selected public key
  - REJECT if verification fails

Step 6: Issuer validation
  - payload["iss"] must exactly match the expected issuer
  - The expected issuer comes from the discovery document, not from the token itself
  - REJECT if mismatch

Step 7: Audience validation
  - payload["aud"] must contain the RP's client_id
  - If payload["aud"] is an array with multiple entries, verify payload["azp"] == client_id
  - REJECT if client_id is not in aud

Step 8: Expiry validation
  - current_time &lt; payload["exp"]  (allow a small clock skew: ≤ 60 seconds)
  - REJECT if token is expired

Step 9: Issued-at validation
  - payload["iat"] should not be too far in the past (IdP-dependent, but flag if &gt; 5 minutes old at receipt)
  - This is advisory, not strictly required, but helps detect old tokens

Step 10: Nonce validation (REQUIRED if nonce was sent)
  - payload["nonce"] must exactly match the nonce stored in the RP's session
  - REJECT if mismatch or if nonce is absent when it was sent in the request
  - Invalidate the stored nonce (single-use)

Step 11: auth_time validation (if max_age was sent)
  - current_time - payload["auth_time"] ≤ max_age (+ clock skew tolerance)
  - REJECT if the user authenticated too long ago

Step 12: acr validation (if acr_values was sent)
  - payload["acr"] must meet the minimum assurance level required
  - REJECT if the achieved acr is insufficient

Step 13: at_hash validation (if access_token was also received)
  - Compute: expected_at_hash = base64url(left_half(sha256(access_token)))
  - payload["at_hash"] must match expected_at_hash
  - REJECT if mismatch

Step 14: c_hash validation (Hybrid Flow, if code was also received)
  - Compute: expected_c_hash = base64url(left_half(sha256(code)))
  - payload["c_hash"] must match expected_c_hash
  - REJECT if mismatch
</code></pre></div></div>

<p>Only after all steps pass should the RP consider the authentication successful and trust the <code class="language-plaintext highlighter-rouge">sub</code> and other identity claims.</p>

<h4 id="72-id-token-vs-access-token-validation">7.2 ID Token vs Access Token Validation</h4>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>ID Token</th>
      <th>Access Token (JWT)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Who validates</strong></td>
      <td>Relying Party (client app)</td>
      <td>Resource Server (API)</td>
    </tr>
    <tr>
      <td><strong>Purpose</strong></td>
      <td>Prove who authenticated</td>
      <td>Prove permission to access resource</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">aud</code> check</strong></td>
      <td>Must contain <code class="language-plaintext highlighter-rouge">client_id</code></td>
      <td>Must contain Resource Server identifier</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">nonce</code> check</strong></td>
      <td>Required (if sent)</td>
      <td>Not applicable</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">at_hash</code> check</strong></td>
      <td>RP validates</td>
      <td>Not applicable</td>
    </tr>
    <tr>
      <td><strong>Scope check</strong></td>
      <td>Not applicable</td>
      <td>RS checks required scope</td>
    </tr>
    <tr>
      <td><strong>Send to API</strong></td>
      <td>NEVER</td>
      <td>Always (in Bearer header)</td>
    </tr>
  </tbody>
</table>

<p>A common misconfiguration: sending the ID Token to an API as a Bearer token. This is wrong because:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">aud</code> of the ID Token is the client, not the API</li>
  <li>The API would need to accept tokens with a different audience claim</li>
  <li>It blurs the authentication/authorization boundary</li>
</ul>

<h4 id="73-local-vs-remote-validation">7.3 Local vs Remote Validation</h4>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Mechanism</th>
      <th>Pros</th>
      <th>Cons</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Local (JWT)</strong></td>
      <td>Verify signature, check claims locally</td>
      <td>Fast, no network call</td>
      <td>Can’t detect revoked tokens; requires JWKS caching</td>
    </tr>
    <tr>
      <td><strong>Remote (Introspection)</strong></td>
      <td>POST to <code class="language-plaintext highlighter-rouge">/introspect</code>, AS responds with token state</td>
      <td>Always current, detects revocation</td>
      <td>Network round-trip on every request</td>
    </tr>
    <tr>
      <td><strong>Hybrid</strong></td>
      <td>Local validation + periodic revocation check</td>
      <td>Balance of speed and freshness</td>
      <td>Complex to implement correctly</td>
    </tr>
  </tbody>
</table>

<p>For most deployments: use <strong>local JWT validation</strong> with short token lifetimes (15–60 min) to bound the revocation window. Implement introspection for high-assurance scenarios where immediate revocation is required.</p>

<hr />

<h3 id="8-end-to-end-oidc-exchange">8. End-to-End OIDC Exchange</h3>

<p>A complete Authorization Code + PKCE + OIDC flow for a confidential web application.</p>

<p><strong>Setup:</strong></p>

<ul>
  <li>IdP: <code class="language-plaintext highlighter-rouge">https://auth.example.com</code></li>
  <li>RP: <code class="language-plaintext highlighter-rouge">https://myapp.com</code> (server-side, confidential client)</li>
  <li><code class="language-plaintext highlighter-rouge">client_id</code>: <code class="language-plaintext highlighter-rouge">myapp-client-id</code></li>
  <li>Requested scopes: <code class="language-plaintext highlighter-rouge">openid profile email offline_access</code></li>
</ul>

<hr />

<h4 id="step-1-discovery-rp-startup--cached">Step 1: Discovery (RP startup / cached)</h4>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/.well-known/openid-configuration</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
</code></pre></div></div>

<p>RP extracts and caches <code class="language-plaintext highlighter-rouge">authorization_endpoint</code>, <code class="language-plaintext highlighter-rouge">token_endpoint</code>, <code class="language-plaintext highlighter-rouge">jwks_uri</code>, <code class="language-plaintext highlighter-rouge">userinfo_endpoint</code>.</p>

<hr />

<h4 id="step-2-pkce-and-nonce-generation-rp-server-side">Step 2: PKCE and Nonce Generation (RP, server-side)</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code_verifier  = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = BASE64URL(SHA256(code_verifier))
             = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
state          = "xK9mP2qR7vL4nJ1w"
nonce          = "n-0S6_WzA2Mj"
</code></pre></div></div>

<p>RP stores <code class="language-plaintext highlighter-rouge">state</code>, <code class="language-plaintext highlighter-rouge">nonce</code>, and <code class="language-plaintext highlighter-rouge">code_verifier</code> in the user’s server-side session (not in the browser).</p>

<hr />

<h4 id="step-3-authorization-request">Step 3: Authorization Request</h4>

<p>RP redirects the user’s browser:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /authorize?
  response_type=code
  &amp;client_id=myapp-client-id
  &amp;redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback
  &amp;scope=openid%20profile%20email%20offline_access
  &amp;state=xK9mP2qR7vL4nJ1w
  &amp;nonce=n-0S6_WzA2Mj
  &amp;code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &amp;code_challenge_method=S256
  &amp;prompt=login
  &amp;max_age=86400
HTTP/1.1
Host: auth.example.com
</span></code></pre></div></div>

<blockquote>
  <p><strong>Internal:</strong> IdP records <code class="language-plaintext highlighter-rouge">code_challenge</code>, <code class="language-plaintext highlighter-rouge">nonce</code>, and session metadata. Displays login + consent UI.</p>
</blockquote>

<hr />

<h4 id="step-4-user-authenticates-and-consents">Step 4: User Authenticates and Consents</h4>

<p>User enters credentials, completes MFA, clicks “Allow.” IdP generates a short-lived authorization code.</p>

<hr />

<h4 id="step-5-authorization-response">Step 5: Authorization Response</h4>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">302</span> <span class="ne">Found</span>
<span class="na">Location</span><span class="p">:</span> <span class="s">https://myapp.com/callback?</span>
<span class="s">  code=SplxlOBeZQQYbYS6WxSbIA</span>
<span class="s">  &amp;state=xK9mP2qR7vL4nJ1w</span>
</code></pre></div></div>

<blockquote>
  <p><strong>RP Action:</strong> Verify <code class="language-plaintext highlighter-rouge">state</code> matches session. Extract <code class="language-plaintext highlighter-rouge">code</code>.</p>
</blockquote>

<hr />

<h4 id="step-6-token-exchange">Step 6: Token Exchange</h4>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/token</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Basic bXlhcHAtY2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=</span>

grant_type=authorization_code
&amp;code=SplxlOBeZQQYbYS6WxSbIA
&amp;redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback
&amp;code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
</code></pre></div></div>

<blockquote>
  <p><strong>IdP Action:</strong> Verifies <code class="language-plaintext highlighter-rouge">client_id</code>/<code class="language-plaintext highlighter-rouge">client_secret</code>. Recomputes <code class="language-plaintext highlighter-rouge">SHA256(code_verifier)</code> and compares to stored <code class="language-plaintext highlighter-rouge">code_challenge</code>. Match confirmed. Generates tokens.</p>
</blockquote>

<hr />

<h4 id="step-7-token-response">Step 7: Token Response</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZ25pbmcta2V5LTIwMjQtMDEifQ.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLWExYjJjM2Q0IiwiYXVkIjoiaHR0cHM6Ly9hcGkubXlhcHAuY29tIiwiZXhwIjoxNzAwMDAzNjAwLCJpYXQiOjE3MDAwMDAwMDAsInNjb3BlIjoicHJvZmlsZSBlbWFpbCJ9.ACCESS_SIG"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8xLOxBtZp8"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ25pbmcta2V5LTIwMjQtMDEifQ.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLWExYjJjM2Q0IiwiYXVkIjoibXlhcHAtY2xpZW50LWlkIiwiZXhwIjoxNzAwMDAzNjAwLCJpYXQiOjE3MDAwMDAwMDAsImF1dGhfdGltZSI6MTY5OTk5OTgwMCwibm9uY2UiOiJuLTBTNl9XekEyTWoiLCJhdF9oYXNoIjoiTVRJek5EVTJOemM0T1RBeE1qTTAiLCJhY3IiOiJ1cm46bWFjZTppbmNvbW1vbjppYXA6c2lsdmVyIiwiYW1yIjpbInB3ZCIsIm1mYSJdLCJuYW1lIjoiU2hhaGVyeWFyIEFobWVkIiwiZW1haWwiOiJzaGFoZXJ5YXJAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0.ID_TOKEN_SIG"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"openid profile email offline_access"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h4 id="step-8-id-token-validation-rp">Step 8: ID Token Validation (RP)</h4>

<p>RP performs all checks from Section 7.1:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✓ Fetch JWKS from https://auth.example.com/jwks
✓ Select key with kid="signing-key-2024-01"
✓ Verify RS256 signature
✓ iss == "https://auth.example.com"  ✓
✓ aud == "myapp-client-id"  ✓
✓ exp (1700003600) &gt; current_time (1700000060)  ✓
✓ nonce == "n-0S6_WzA2Mj" matches session  ✓  → invalidate stored nonce
âerified against access_token  ✓
✓ auth_time: 1699999800 — authenticated 200s ago, within max_age=86400  ✓
✓ acr: "silver" — MFA confirmed  ✓
→ Authentication successful. User identity: sub=user-a1b2c3d4
</code></pre></div></div>

<hr />

<h4 id="step-9-optional-userinfo-call">Step 9: Optional UserInfo Call</h4>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/userinfo</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">auth.example.com</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s">Bearer eyJhbGciOiJSUzI1NiJ9.ACCESS_TOKEN...</span>
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user-a1b2c3d4"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shaheryar Ahmed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"given_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shaheryar"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"family_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ahmed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shaheryar@example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email_verified"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"picture"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://auth.example.com/users/user-a1b2c3d4/photo.jpg"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>RP Action:</strong> Verify <code class="language-plaintext highlighter-rouge">sub</code> matches ID Token <code class="language-plaintext highlighter-rouge">sub</code>. If mismatch → REJECT. Otherwise, use additional profile data.</p>
</blockquote>

<hr />

<h3 id="9-security-deep-dive">9. Security Deep Dive</h3>

<h4 id="91-id-token-replay-attacks-nonce-misuse">9.1 ID Token Replay Attacks (Nonce Misuse)</h4>

<h5 id="the-attack">The Attack</h5>

<p>An attacker captures a valid ID Token (from logs, from a compromised RP, or from a previous authentication). They inject this token into a new authentication session.```</p>
<ol>
  <li>Victim authenticates → IdP issues ID Token with nonce=”A”</li>
  <li>Attacker captures the ID Token (e.g., from logs)</li>
  <li>Attacker later initiates a new auth flow with the RP</li>
  <li>Attacker intercepts the callback and substitutes the captured ID Token</li>
  <li>If RP doesn’t validate nonce, RP accepts the old token as fresh authentication
```</li>
</ol>

<h5 id="impact">Impact</h5>

<p>Authentication bypass. Attacker is logged in as the victim.</p>

<h5 id="mitigation">Mitigation</h5>

<ul>
  <li>Always generate and validate <code class="language-plaintext highlighter-rouge">nonce</code></li>
  <li>The <code class="language-plaintext highlighter-rouge">nonce</code> in the ID Token must match the o generated for that specific authentication session</li>
  <li>Nonces must be single-use — invalidate after successful validation</li>
  <li>Nonces must be cryptographically random (not sequential or user-derived)</li>
</ul>

<hr />

<h4 id="92-improper-id-token-validation">9.2 Improper ID Token Validation</h4>

<h5 id="the-attack-1">The Attack</h5>

<p>RPs that partially validate ID Tokens — or skip validation entirely when tokens are received from “trusted” sources — are vulnerable.</p>

<p>Common omissions:</p>

<ul>
  <li>Skipping signature verification (“we got it from the HTTPS token endpoint, it must be safe”)
validating <code class="language-plaintext highlighter-rouge">aud</code> — token from one RP accepted by another</li>
  <li>Not validating <code class="language-plaintext highlighter-rouge">exp</code> — accepting expired tokens</li>
  <li>Not validating <code class="language-plaintext highlighter-rouge">nonce</code></li>
</ul>

<h5 id="impact-1">Impact</h5>

<ul>
  <li>Missing <code class="language-plaintext highlighter-rouge">aud</code> check → token issued for App A is accepted by App B (cross-site token confusion)</li>
  <li>Missing <code class="language-plaintext highlighter-rouge">exp</code> check → stolen tokens usable indefinitely</li>
  <li>Missing signature check → forged tokens accepted</li>
</ul>

<h5 id="mitigation-1">Mitigation</h5>

<p>Execute all 14 validation steps in Section 7.1. No exceptions. Use a well-tested OIDC library rather than hand-rolling validation. Like <code class="language-plaintext highlighter-rouge">python-jose</code>, <code class="language-plaintext highlighter-rouge">nimbus-jose-jwt</code>, <code class="language-plaintext highlighter-rouge">jose</code> (Node.js) handle the full validation chain.</p>

<hr />

<h4 id="93-signature-verification-bypass-algnone-and-algorithm-confusion">9.3 Signature Verification Bypass (<code class="language-plaintext highlighter-rouge">alg=none</code> and Algorithm Confusion)</h4>

<h5 id="attack-1-algnone">Attack 1: <code class="language-plaintext highlighter-rouge">alg=none</code></h5>

<p>Some JWT libraries, if misconfigured, accept a token with <code class="language-plaintext highlighter-rouge">"alg": "none"</code> and no signature — treating it as valid:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Header</span><span class="w">
</span><span class="p">{</span><span class="w"> </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none"</span><span class="p">,</span><span class="w"> </span><span class="nl">"typ"</span><span class="p">:</span><span class="w"> </span><span class="s2">"JWT"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">Payload</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="err">any</span><span class="w"> </span><span class="err">claims</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">attacker</span><span class="w"> </span><span class="err">wants</span><span class="w">
</span><span class="p">{</span><span class="w"> </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span><span class="p">,</span><span class="w"> </span><span class="nl">"aud"</span><span class="p">:</span><span class="w"> </span><span class="s2">"myapp-client-id"</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">Signature</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="err">empty</span><span class="w"> </span><span class="err">string</span><span class="w">
</span></code></pre></div></div>

<p>This requires noographic key. If the library accepts it, the attacker can forge any identity.</p>

<h5 id="attack-2-rs256--hs256-algorithm-confusion">Attack 2: RS256 → HS256 Algorithm Confusion</h5>

<p>Some libraries accept either RSA (asymmetric) or HMAC (symmetric) algorithms. An attacker:</p>

<ol>
  <li>Takes the IdP’s <strong>public RSA key</strong> (publicly available in JWKS)</li>
  <li>Signs a forged JWT using that public key as an <strong>HMAC secret</strong> with <code class="language-plaintext highlighter-rouge">alg=HS256</code></li>
  <li>Submits to an RP whose library uses the public key for both RS256 verification and HS256 verification without distinguishing</li>
</ol>

<p>The librarverifies the HMAC signature using the “public key” as the secret — and it matches, because the attacker used it to sign.</p>

<h5 id="impact-2">Impact</h5>

<p>Complete authentication bypass. Attacker can forge any identity.</p>

<h5 id="mitigation-2">Mitigation</h5>

<ul>
  <li><strong>Hardcode the expected algorithm</strong> in the RP’s configuration — never accept <code class="language-plaintext highlighter-rouge">alg</code> from the token header without allowlisting</li>
  <li>Explicitly <strong>reject <code class="language-plaintext highlighter-rouge">alg=none</code></strong></li>
  <li>Use separate key material for HMAC and RSA operations</li>
  <li>Use well-maintained libraries with secure defaults (modern versions rt <code class="language-plaintext highlighter-rouge">none</code> by default)</li>
  <li>Configure: <code class="language-plaintext highlighter-rouge">allowed_algorithms = ["RS256", "ES256"]</code> — not <code class="language-plaintext highlighter-rouge">["RS256", "HS256", "none"]</code></li>
</ul>

<hr />

<h4 id="94-key-confusion-attacks-jwks-misuse">9.4 Key Confusion Attacks (JWKS Misuse)</h4>

<h5 id="the-attack-2">The Attack</h5>

<p>The RP fetches the wrong JWKS, uses the wrong key, or is tricked into trusting attacker-controlled keys.</p>

<p>Scenarios:</p>

<ul>
  <li><strong>JWKS URL injection:</strong> If the RP derives the JWKS URL from the <code class="language-plaintext highlighter-rouge">iss</code> claim in the token (rather than from the pre-configured discovery document), an attacker sets <code class="language-plaintext highlighter-rouge">"iss": "https://attacker.com"</code>, and the RP fetes keys from <code class="language-plaintext highlighter-rouge">https://attacker.com/jwks</code> — keys the attacker controls</li>
  <li><strong>kid confusion:</strong> Multiple keys in JWKS — attacker crafts a token with a <code class="language-plaintext highlighter-rouge">kid</code> pointing to a key they control (if the JWKS is somehow tampered with)</li>
  <li><strong>Stale key cache attack:</strong> The RP caches old keys indefinitely — a key compromise is not reflected</li>
</ul>

<h5 id="mitigation-3">Mitigation</h5>

<ul>
  <li><strong>Never derive JWKS URL from the token</strong> — always use the URL from the pre-configured or cached discovery document</li>
  <li><strong>Validate <code class="language-plaintext highlighter-rouge">iss</code> against the expected issue selecting a key</strong> — not after</li>
  <li>Implement JWKS cache TTL and re-fetch on <code class="language-plaintext highlighter-rouge">kid</code> miss (but no more than once per request to prevent DoS)</li>
  <li>Pin the issuer URL — do not dynamically trust new issuers</li>
</ul>

<hr />

<h4 id="95-authorization-code-interception-oidc-context">9.5 Authorization Code Interception (OIDC Context)</h4>

<p>Same mechanism as in OAuth 2.0, but with additional OIDC-specific consequences: the attacker who intercepts the code also receives the ID Token from the token endpoint, gaining the user’s identity.</p>

<h5 id="mitigation-4">Mitigation</h5>

<p>PKCE is the primary control. In OIpecifically:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">nonce</code> provides a secondary binding — even if the code is intercepted and exchanged, the attacker’s session won’t have the correct <code class="language-plaintext highlighter-rouge">nonce</code> in state to validate the returned ID Token</li>
  <li>This is why both PKCE and <code class="language-plaintext highlighter-rouge">nonce</code> should always be used together</li>
</ul>

<hr />

<h4 id="96-mix-up-attacks">9.6 Mix-Up Attacks</h4>

<h5 id="the-attack-3">The Attack</h5>

<p>The mix-up attack targets deployments where an RP supports <strong>multiple IdPs</strong>. An attacker operates a malicious IdP.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. RP supports: auth.example.com (legitimate) and attacker.com (malicious)
2User intends to authenticate with auth.example.com
3. Attacker intercepts the authorization request and redirects it to attacker.com
4. attacker.com completes a flow and issues an authorization code for attacker.com
5. The RP's callback receives a code — but doesn't know which IdP issued it
6. RP sends the code to auth.example.com's token endpoint (the legitimate one)
7. auth.example.com rejects the code — but the error reveals information
   OR: the RP confuses which IdP's token endpoint to use, sendine code to attacker.com
8. attacker.com issues a token for a victim user (whose code was obtained elsewhere)
</code></pre></div></div>

<h5 id="impact-3">Impact</h5>

<p>Cross-IdP token confusion; in some scenarios, authentication as a different user.</p>

<h5 id="mitigation-5">Mitigation</h5>

<ul>
  <li><strong>Issuer binding:</strong> The RP must track which IdP initiated each authentication session and validate that the callback is from the expected IdP</li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">iss</code> parameter in authorization response</strong> (RFC 9207) — the authorization server includes its <code class="language-plaintext highlighter-rouge">issuer</code> in the authorization response, aowing the RP to verify the response came from the expected IdP:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://myapp.com/callback?code=...&amp;state=...&amp;iss=https%3A%2F%2Fauth.example.com
</code></pre></div>    </div>
  </li>
  <li>Store the expected <code class="language-plaintext highlighter-rouge">iss</code> in the session alongside <code class="language-plaintext highlighter-rouge">state</code> and <code class="language-plaintext highlighter-rouge">nonce</code></li>
</ul>

<hr />

<h4 id="97-token-substitution-attacks">9.7 Token Substitution Attacks</h4>

<h5 id="the-attack-4">The Attack</h5>

<p>In flows that return multiple tokens (Hybrid Flow), an attacker substitutes a token from one flow into another.</p>

<p>Example: Attacker has a valid ID Token issued to them by the IdP. They substitute it into the callback for a victim’s ongoing Hybrid Flow, tricking the RP into authenticating as the attacker rather than the victim.</p>

<h5 id="mitigation-6">Mitigation</h5>

<ul>
  <li><code class="language-plaintext highlighter-rouge">c_hash</code> in the ID Token from the auth endpoint binds it to the specific authorization code — verify it</li>
  <li><code class="language-plaintext highlighter-rouge">at_hash</code> binds the ID Token to the specific access token — verify it</li>
  <li>These hash bindings make substitution detectable: a substituted ID Token’s <code class="language-plaintext highlighter-rouge">c_hash</code> won’t match the victim’s code</li>
</ul>

<hr />

<h4 id="98-confusion-between-access-token-and-id-token">9.8 Confusion Between Access Token and ID Token</h4>

<h5 id="the-attack-5">The Attack</h5>

<p>A develouses the access token as proof of identity (checking if it’s valid and concluding they know who the user is), or sends the ID Token to an API as a Bearer token.</p>

<p><strong>Scenario A:</strong> RP sends ID Token to Resource Server as Bearer token. RS is configured to accept any token from the IdP, validates the signature, and accepts it — but the <code class="language-plaintext highlighter-rouge">aud</code> check would fail (<code class="language-plaintext highlighter-rouge">aud</code> is the client ID, not the RS identifier). If the RS skips <code class="language-plaintext highlighter-rouge">aud</code> validation, it accepts tokens not intended for it.</p>

<p><strong>Scenario B:</strong> RP trusts any vid access token as proof of user identity without verifying the ID Token, not checking <code class="language-plaintext highlighter-rouge">sub</code>, <code class="language-plaintext highlighter-rouge">nonce</code>, or <code class="language-plaintext highlighter-rouge">auth_time</code>.</p>

<h5 id="impact-4">Impact</h5>

<p>In Scenario A: horizontal privilege escalation if RS accepts tokens from other clients.<br />
In Scenario B: authentication is reduced to “does a valid token exist” — bypassing user identity verification.</p>

<h5 id="mitigation-7">Mitigation</h5>

<ul>
  <li>ID Token → used exclusively by the RP to identify the user. Never forward to an API.</li>
  <li>Access Token → used exclusively to call APIs. Contains no reliabltity for the RP.</li>
  <li>Resource Servers must validate <code class="language-plaintext highlighter-rouge">aud</code> strictly — tokens intended for other audiences must be rejected.</li>
</ul>

<hr />

<h4 id="99-open-redirect-exploitation-in-oidc">9.9 Open Redirect Exploitation in OIDC</h4>

<p>OIDC flows involve multiple redirects. Open redirect vulnerabilities can allow:</p>

<ul>
  <li>Stealing authorization codes: redirect the callback to an attacker URL that captures the <code class="language-plaintext highlighter-rouge">code</code> parameter</li>
  <li>Stealing ID Tokens (Hybrid/Implicit): if <code class="language-plaintext highlighter-rouge">response_mode=fragment</code> or <code class="language-plaintext highlighter-rouge">response_mode=query</code>, tokens in the redirect URL go to the attacker</li>
</ul>

<p>The attacsurface is the <code class="language-plaintext highlighter-rouge">redirect_uri</code> parameter. If the IdP allows partial matching or wildcards, an attacker can direct the response to their server.</p>

<h5 id="mitigation-8">Mitigation</h5>

<ul>
  <li>Exact redirect URI matching (covered in OAuth 2.0 security section)</li>
  <li>In OIDC specifically: PKCE + <code class="language-plaintext highlighter-rouge">nonce</code> provide defense-in-depth — even if the code is delivered to the attacker, they can’t exchange it (no <code class="language-plaintext highlighter-rouge">code_verifier</code>) and can’t use the ID Token (no <code class="language-plaintext highlighter-rouge">nonce</code> match)</li>
</ul>

<hr />

<h4 id="910-pkce-misconfiguration-in-oidc-flows">9.10 PKCE Misconfiguration in OIDC Flows</h4>

<p>When PKCE is required bunot enforced, or when <code class="language-plaintext highlighter-rouge">plain</code> method is allowed instead of <code class="language-plaintext highlighter-rouge">S256</code>:</p>

<ul>
  <li><strong>No PKCE enforcement:</strong> Authorization code can be exchanged by anyone who intercepts it — no <code class="language-plaintext highlighter-rouge">code_verifier</code> required. PKCE becomes theater.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">plain</code> method:</strong> <code class="language-plaintext highlighter-rouge">code_challenge = code_verifier</code> — if an attacker intercepts the authorization request, they capture both the challenge (= verifier) and can use it in the token exchange.</li>
  <li><strong>Weak verifier entropy:</strong> Predictable or short verifiers are brute-forceable within the code’s shortetime.</li>
</ul>

<h5 id="mitigation-9">Mitigation</h5>

<ul>
  <li>IdP must enforce that if <code class="language-plaintext highlighter-rouge">code_challenge</code> was sent, <code class="language-plaintext highlighter-rouge">code_verifier</code> is required in the token exchange</li>
  <li>Only allow <code class="language-plaintext highlighter-rouge">S256</code> — remove <code class="language-plaintext highlighter-rouge">plain</code> from supported methods</li>
  <li>Minimum verifier length: 43 characters from a cryptographically secure random source</li>
  <li>Best practice: require PKCE for all flows, including confidential clients</li>
</ul>

<hr />

<h3 id="10-best-practices-and-modern-recommendations">10. Best Practices and Modern Recommendations</h3>

<h4 id="101-flow-selection">10.1 Flow Selection</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User-facing authentication (web app, mobile, SPA):
  → Authorization Co PKCE + OIDC
  → Always include nonce, state, code_challenge (S256)

Machine-to-machine (no user):
  → OAuth 2.0 Client Credentials
  → OIDC does not apply (no user to identify)

Input-constrained device:
  → Device Code Flow + OIDC
  → Include nonce in the device authorization request

NEVER use:
  → Implicit Flow
  → ROPC / Password Grant
  → Hybrid Flow (unless you have a specific architectural requirement)
</code></pre></div></div>

<h4 id="102-id-token-validation-checklist">10.2 ID Token Validation Checklist</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>□ Signature verified using IdP blic key from JWKS
□ Algorithm is in the configured allowlist (not "none", not unexpected alg)
□ kid-based key selection (not try-all-keys)
□ iss exactly matches expected issuer from discovery document
□ aud contains client_id
□ exp &gt; current_time (with ≤60s clock skew tolerance)
□ nonce matches session-stored value (if sent)
□ Nonce invalidated after use (single-use)
□ at_hash verified (if access_token received)
□ c_hash verified (if Hybrid Flow)
□ auth_time checked against max_age (i)
□ acr meets minimum requirement (if sent)
□ sub from UserInfo matches sub from ID Token
</code></pre></div></div>

<h4 id="103-secure-parameter-handling">10.3 Secure Parameter Handling</h4>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Requirement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nonce</code></td>
      <td>Cryptographically random, ≥128 bits, server-side storage, single-use</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">state</code></td>
      <td>Cryptographically random, ≥128 bits, server-side storage</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">code_verifier</code></td>
      <td>Cryptographically random, 43–128 chars, server-side only</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">redirect_uri</code></td>
      <td>Exact match against registered URIs; HTTPS only in production</td>
    </tr>
    <tr>
      <td>`client_secer in URLs, JS code, or mobile binaries; server-side confidential clients only</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h4 id="104-token-storage">10.4 Token Storage</h4>

<table>
  <thead>
    <tr>
      <th>Token</th>
      <th>Storage Location</th>
      <th>Never Store In</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Access Token</td>
      <td>Memory (SPA), HttpOnly cookie (server)</td>
      <td>localStorage, sessionStorage, URL</td>
    </tr>
    <tr>
      <td>Refresh Token</td>
      <td>Server-side session, HttpOnly cookie</td>
      <td>localStorage, JS-accessible storage</td>
    </tr>
    <tr>
      <td>ID Token</td>
      <td>Server-side session (after validation)</td>
      <td>localStorage, URL, client-side JS</td>
    </tr>
  </tbody>
</table>

<h4 id="105-scope-and-claims-minimization">10.5 Scope and Claims Minimization</h4>

<ul>
  <li>Request only the scopes needed for the immediate operation (incremental authorization)</li>
  <li>Do not request <code class="language-plaintext highlighter-rouge">profile</code> if you only need <code class="language-plaintext highlighter-rouge">sub</code> and <code class="language-plaintext highlighter-rouge">email</code></li>
  <li>Use <code class="language-plaintext highlighter-rouge">offline_access</code> only when the application genuinely needs background access</li>
  <li>Configure the IdP to issue minimal claims in ID Tokens; use UserInfo for supplemental data</li>
  <li>Custom claims with sensitive data should be in encrypted ID Tokens (JWE) if they must be in the token</li>
</ul>

<h4 id="106-session-management-and-logout">10.6 Session Management and Logout</h4>

<p>OIDC defines three logout mechanisms:</p>

<table>
  <thead>
    <tr>
      <th>Mechanism</th>
      <th>How It Works</th>
      <th>When To Use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>RP-Initiated Logout</strong></td>
      <td>RP redirects user to IdP’s <code class="language-plaintext highlighter-rouge">end_session_endpoint</code></td>
      <td>When user explicitly logs out</td>
    </tr>
    <tr>
      <td><strong>Front-Channel Logout</strong></td>
      <td>IdP loads RPs’ logout URLs in iframes</td>
      <td>When IdP needs to log out user from all RPs simultaneously</td>
    </tr>
    <tr>
      <td><strong>Back-Channel Logout</strong></td>
      <td>IdP POSTs a signed logout token to each RP’s registered URL</td>
      <td>Most reliable; not browser-dependent; preferred</td>
    </tr>
  </tbody>
</table>

<p>Back-channel logout is the modern recommendation. The logout token is a signed JWT (similar to an ID Token) containing <code class="language-plaintext highlighter-rouge">sub</code> and <code class="language-plaintext highlighter-rouge">sid</code>, allowing the RP to invalidate the corresponding session server-side without user browser involvement.</p>

<h4 id="107-library-and-implementation-guidance">10.7 Library and Implementation Guidance</h4>

<p>Do not implement OIDC validation from scratch in production. Use audited libraries:</p>

<table>
  <thead>
    <tr>
      <th>Language</th>
      <th>Recommended Library</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Python</td>
      <td><code class="language-plaintext highlighter-rouge">authlib</code>, <code class="language-plaintext highlighter-rouge">python-jose</code>, <code class="language-plaintext highlighter-rouge">oic</code></td>
    </tr>
    <tr>
      <td>Node.js</td>
      <td><code class="language-plaintext highlighter-rouge">openid-client</code>, <code class="language-plaintext highlighter-rouge">jose</code></td>
    </tr>
    <tr>
      <td>Java</td>
      <td><code class="language-plaintext highlighter-rouge">nimbus-jose-jwt</code>, Spring Security OAuth2</td>
    </tr>
    <tr>
      <td>.NET</td>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Identity.Web</code>, <code class="language-plaintext highlighter-rouge">IdentityModel</code></td>
    </tr>
    <tr>
      <td>Go</td>
      <td><code class="language-plaintext highlighter-rouge">coreos/go-oidc</code></td>
    </tr>
  </tbody>
</table>

<p>When evaluating a library, verify it:</p>

<ul>
  <li>Rejects <code class="language-plaintext highlighter-rouge">alg=none</code></li>
  <li>Validates <code class="language-plaintext highlighter-rouge">iss</code>, <code class="language-plaintext highlighter-rouge">aud</code>, <code class="language-plaintext highlighter-rouge">exp</code>, <code class="language-plaintext highlighter-rouge">nonce</code> by default</li>
  <li>Uses <code class="language-plaintext highlighter-rouge">kid</code>-based key selection</li>
  <li>Supports automatic JWKS refresh</li>
</ul>

<hr />

<h3 id="quick-reference">Quick Reference</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OIDC EXTENDS OAUTH 2.0 BY ADDING:
  openid scope      → triggers OIDC
  id_token          → JWT asserting user identity
  nonce parameter   → ID Token replay protection
  UserInfo endpoint → additional identity claims
  Discovery doc     → self-configuringel

ID TOKEN REQUIRED CLAIMS:
  iss  sub  aud  exp  iat
  + nonce (if sent)  + auth_time (if max_age sent)

ID TOKEN CRITICAL VALIDATION:
  1. Signature (RS256/ES256, using JWKS)
  2. iss == expected issuer
  3. aud ∋ client_id
  4. exp &gt; now
  5. nonce == session nonce (invalidate after)
  6. at_hash matches access_token

KEY SECURITY RULES:
  → Never accept alg=none
  → Never derive JWKS URL from token claims
  → Never use ID Token as Bearer token to APIs
  → Never use access token as proof of i→ Always validate nonce
  → Always use PKCE + S256
  → Always validate ALL 14 steps — no shortcuts
</code></pre></div></div>

<hr />

<h3 id="references">References</h3>

<ul>
  <li><a href="https://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core 1.0</a></li>
  <li><a href="https://openid.net/specs/openid-connect-discovery-1_0.html">OpenID Connect Discovery 1.0</a></li>
  <li><a href="https://openid.net/specs/openid-connect-rpinitiated-1_0.html">OpenID Connect RP-Initiated Logout</a></li>
  <li><a href="https://openid.net/specs/openid-connect-backchannel-1_0.html">OpenID Connect Back-Channel Logout</a></li>
  <li>9 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)</li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc7517">RFC 7517 — JSON Web Key (JWK)</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc7518">RFC 7518 — JSON Web Algorithms (JWA)</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207 — OAuth 2.0 Authorization Server Issuer Identification</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics">OAuth 2.0 Security BCP</a></li>
  <li><a href="httpp.org/www-project-web-security-testing-guide/">OWASP Testing for OAuth/OIDC Weaknesses</a></li>
  <li><a href="https://portswigger.net/web-security/oauth">PortSwigger — OAuth 2.0 Authentication Vulnerabilities</a></li>
  <li><a href="https://jwt.io">jwt.io — JWT Debugger</a></li>
</ul>]]></content><author><name>shaheerkj</name></author><category term="Security" /><category term="Cloud Security" /><category term="OIDC" /><category term="OAuth2.0" /><category term="cloud security" /><category term="authentication" /><category term="IAM" /><category term="Entra ID" /><category term="Identity Management" /><summary type="html"><![CDATA[A Comprehensive explanation and Deep dive of the Open ID Connect protocol. Built on top of OAuth2.0 and supports modern authentication.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.shaheerkj.me/%7B%22path%22=%3Enil%7D" /><media:content medium="image" url="https://blog.shaheerkj.me/%7B%22path%22=%3Enil%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">PHS vs PTA vs ADFS — How Enterprises Actually Authenticate (And Why It Matters)</title><link href="https://blog.shaheerkj.me/posts/phs-vs-pta-vs-adfs-how-enterprises-actually-authenticate/" rel="alternate" type="text/html" title="PHS vs PTA vs ADFS — How Enterprises Actually Authenticate (And Why It Matters)" /><published>2026-03-18T00:00:00+00:00</published><updated>2026-04-29T18:55:50+00:00</updated><id>https://blog.shaheerkj.me/posts/phs-vs-pta-vs-adfs-how-enterprises-actually-authenticate</id><content type="html" xml:base="https://blog.shaheerkj.me/posts/phs-vs-pta-vs-adfs-how-enterprises-actually-authenticate/"><![CDATA[<p>HYBRID IDENTITY LAB
Deploying Windows AD in Azure &amp; Connecting to Entra ID
ADFS  ·  PTA  ·  PHS  ·  Entra Cloud Sync  ·  Hybrid Identity
Prerequisites: Azure Subscription + M365 Developer Tenant (Entra ID)</p>

<h2 id="overview--architecture">Overview &amp; Architecture</h2>
<p>This lab deploys a simulated on-premises Windows Active Directory environment inside Azure (using a VM), then connects it to your existing Entra ID tenant using three different hybrid identity methods. By the end you will have hands-on experience with every major hybrid identity pattern used in real enterprise environments.</p>

<h3 id="what-you-will-build">What You Will Build</h3>

<h3 id="architecture-diagram-text">Architecture Diagram (Text)</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Azure Subscription
  └── Resource Group: rg-hybrid-lab
        ├── VNet: vnet-lab (10.0.0.0/16)
        │     └── Subnet: snet-dc (10.0.1.0/24)
        ├── VM-DC01  (Windows Server 2022 — Domain Controller)
        │     ├── ADDS Role
        │     ├── Azure AD Connect installed
        │     └── Entra Cloud Sync agent
        └── VM-ADFS01  (Windows Server 2022 — Federation Server)
              └── ADFS Role
Entra ID Tenant (your M365 Dev Tenant)
  └── Synced users from lab.local domain
  └── Hybrid identity authentication methods configured
</code></pre></div></div>

<h3 id="cost-estimate">Cost Estimate</h3>
<p>Two B2s VMs (~$0.048/hr each) + storage. Total: ~$5–8 for a full weekend lab if you deallocate VMs when not in use. Always stop (deallocate) VMs when not actively working.
⚠ Warning:  Stop and deallocate both VMs when not in use. Do not just stop them — deallocate. Otherwise Azure still charges compute.</p>

<h2 id="phase-1--azure-infrastructure-setup">Phase 1 — Azure Infrastructure Setup</h2>

<h3 id="step-11--create-resource-group-and-vnet">Step 1.1 — Create Resource Group and VNet</h3>
<p>Everything for this lab lives in one resource group. This makes cleanup easy — delete the resource group and everything goes.</p>
<ul>
  <li>In Azure Portal → Resource Groups → Create</li>
  <li>Name: rg-hybrid-lab - Region: choose one close to you (e.g. West Europe)</li>
  <li>Go to Virtual Networks → Create</li>
  <li>Name: vnet-lab - Address space: 10.0.0.0/16</li>
  <li>Add subnet: snet-dc - Address range: 10.0.1.0/24</li>
  <li>Review + Create</li>
</ul>

<h3 id="step-12--deploy-the-domain-controller-vm-vm-dc01">Step 1.2 — Deploy the Domain Controller VM (VM-DC01)</h3>
<ul>
  <li>Go to Virtual Machines → Create → Azure Virtual Machine</li>
  <li>Resource group: rg-hybrid-lab</li>
  <li>VM name: VM-DC01</li>
  <li>Image: Windows Server 2022 Datacenter — Gen2</li>
  <li>Size: Standard_B2s (2 vCPUs, 4GB RAM — cheapest viable option)</li>
  <li>Administrator username: labadmin - Password: something strong, write it down</li>
  <li>Under Networking — select vnet-lab and snet-dc subnet</li>
  <li>Public IP: create one (needed for RDP access)</li>
  <li>Under Inbound ports — allow RDP (3389) — we will lock this down after</li>
  <li>Review + Create → Create
💡 Tip:  While VM-DC01 deploys, you can read ahead to Phase 1.3 to understand what you’re about to configure.</li>
</ul>

<h3 id="step-13--configure-static-private-ip">Step 1.3 — Configure Static Private IP</h3>
<p>Domain Controllers must have a static IP — DNS registration breaks if the IP changes.</p>
<ul>
  <li>Go to VM-DC01 → Networking → Network Interface</li>
  <li>IP Configurations → click ipconfig1</li>
  <li>Change Assignment from Dynamic to Static</li>
  <li>Set IP to: 10.0.1.4 → Save</li>
</ul>

<h3 id="step-14--install-active-directory-domain-services">Step 1.4 — Install Active Directory Domain Services</h3>
<ul>
  <li>RDP into VM-DC01 using its public IP, username: labadmin</li>
  <li>Open Server Manager → Add Roles and Features</li>
  <li>Select: Active Directory Domain Services → Add Features → Install</li>
  <li>After install, click the flag notification → Promote this server to a domain controller</li>
  <li>Select: Add a new forest</li>
  <li>Root domain name: lab.local</li>
  <li>Forest functional level: Windows Server 2016</li>
  <li>Set DSRM password (write this down — needed for AD recovery)</li>
  <li>Accept default paths → Install → Server will reboot automatically</li>
  <li>After reboot, RDP back in as: lab\labadmin
💡 Tip:  lab.local is a non-routable domain used purely for lab purposes. In production, you would use a real domain you own (e.g. yourdomain.com).</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/installing-adds.png" alt="adds" /></p>

<p><img src="/assets/img/4-authentication-methods/adds-installation-done.png" alt="installation-finished" /></p>

<h3 id="step-15--create-lab-users-and-ous-in-active-directory">Step 1.5 — Create Lab Users and OUs in Active Directory</h3>
<p>Create a realistic OU structure — this matters when you configure sync scope later.</p>
<ul>
  <li>Open Active Directory Users and Computers (ADUC) on VM-DC01</li>
  <li>Right-click lab.local → New → Organizational Unit → Name: LabUsers</li>
  <li>Create another OU: LabAdmins</li>
  <li>Inside LabUsers, create 3 test users:</li>
  <li>Right-click LabUsers → New → User</li>
  <li>User 1: First: Alice - Last: Smith - UPN: alice.smith@lab.local - Password: Lab@12345!</li>
  <li>User 2: Bob Jones - bob.jones@lab.local</li>
  <li>User 3: Carol Lee - carol.lee@lab.local</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/lab-users.png" alt="lab-users" /></p>

<ul>
  <li>Inside LabAdmins, create: Admin User - admin.user@lab.local — add to Domain Admins group</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/lab-admins.png" alt="lab-admin" /></p>

<h3 id="step-16--configure-vnet-dns-to-point-to-dc">Step 1.6 — Configure VNet DNS to point to DC</h3>
<p>Azure VMs use Azure DNS by default. We need them to use our DC for AD DNS resolution.</p>
<ul>
  <li>Go to vnet-lab → DNS Servers</li>
  <li>Change to Custom → Enter 10.0.1.4 (VM-DC01 static IP)</li>
  <li>Save — restart VM-DC01 to pick up the DNS change</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/custom-dns-server.png" alt="custom-dns" /></p>

<h2 id="phase-2--password-hash-sync-phs">Phase 2 — Password Hash Sync (PHS)</h2>

<h3 id="what-phs-actually-does">What PHS Actually Does</h3>
<p>PHS is the simplest hybrid identity method. Azure AD Connect takes a hash of the password hash stored in AD and syncs it to Entra ID. Authentication happens entirely in the cloud — Entra ID validates the credential without contacting your on-prem DC. If your on-prem goes down, users can still sign in.
Conceptual Note:  Microsoft hashes the password hash again before syncing — so Entra ID never has the original password or even the direct AD hash. It is a hash of a hash with a salt. PHS is considered secure and is the recommended default sync method for most organizations.</p>

<h3 id="step-21--add-a-custom-domain-to-entra-id">Step 2.1 — Add a Custom Domain to Entra ID</h3>
<p>You cannot sync lab.local to Entra ID — .local is non-routable. You need to add a UPN suffix that matches a domain you own OR use the default onmicrosoft.com domain.</p>
<ul>
  <li>In Entra ID portal → Custom domain names → Add custom domain</li>
  <li>If you own a domain (example.com etc.), add it and verify via DNS TXT record</li>
  <li>If not, skip this — Azure AD Connect will use the @xxx.onmicrosoft.com suffix</li>
  <li>Back in VM-DC01 → Tools → Active Directory Domains and Trusts → Right-click (Active Directory Domains and Trusts) → Properties → UPN Suffixes</li>
  <li>Add your verified domain or your onmicrosoft.com domain as an alternative UPN suffix</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/adding-domain.png" alt="custom-domain" /></p>
<ul>
  <li>Update your test users’ UPN to use the new suffix: alice.smith@xxx.onmicrosoft.com</li>
</ul>

<h3 id="step-22--install-azure-ad-connect-on-vm-dc01">Step 2.2 — Install Azure AD Connect on VM-DC01</h3>
<ul>
  <li>On VM-DC01, open a browser and go to: entra.microsoft.com</li>
  <li>In left pane, Entra ID → Entra Connect → Get Started → Manage → Download Connect Sync Agent</li>
  <li>Download and run the Azure AD Connect installer</li>
  <li>Accept license terms → Use express settings (for lab purposes)</li>
  <li>Connect to Entra ID — enter your Entra ID Global Admin credentials (xxx@xxx.onmicrosoft.com)</li>
  <li>Connect to ADDS — enter: lab\labadmin and password</li>
  <li>Azure AD sign-in configuration — you will see a warning that lab.local is not verified — acknowledge and continue</li>
  <li>On the sync method screen — confirm Password Hash Synchronization is selected</li>
  <li>Check: Start the synchronization process when configuration completes</li>
  <li>Install → Configuration completes → Initial sync runs</li>
</ul>

<h3 id="step-23--verify-sync-in-entra-id">Step 2.3 — Verify Sync in Entra ID</h3>
<ul>
  <li>Go to Entra ID portal → Users</li>
  <li>You should see alice.smith, bob.jones, carol.lee appear as synced users</li>
  <li>Click on alice.smith — note the Source field shows: Windows Server AD</li>
  <li>Go to Entra ID → Azure AD Connect (under Hybrid management) — verify sync status shows Enabled
💡 Tip:  Sync runs every 30 minutes by default. To force an immediate sync: on VM-DC01 open PowerShell and run:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Start-ADSyncSyncCycle -PolicyType Delta
</code></pre></div>    </div>
  </li>
</ul>

<p><img src="/assets/img/4-authentication-methods/successful-phs-sync.png" alt="users-synced" /></p>

<h3 id="step-24--test-phs-authentication">Step 2.4 — Test PHS Authentication</h3>
<ul>
  <li>Open a private browser window → go to portal.azure.com</li>
  <li>Sign in as alice.smith@xxx.onmicrosoft.com with password Lab@12345!</li>
  <li>Authentication should succeed — Entra ID validated against the synced password hash</li>
  <li>Check Entra ID → Sign-in logs — you will see the sign-in with authentication method: Password Hash Sync
🔴 Important:  For PHS to work, the UPN of the synced user must match a verified domain in Entra ID. If you see authentication failures, the UPN suffix mismatch is the most likely cause.</li>
</ul>

<p>Logging in as as alice smith
<img src="/assets/img/4-authentication-methods/alice-smith-phs-login.png" alt="logged-in-as-alice-smith" /></p>

<h2 id="phase-3--pass-through-authentication-pta">Phase 3 — Pass-Through Authentication (PTA)</h2>

<h3 id="what-pta-actually-does">What PTA Actually Does</h3>
<p>Unlike PHS where authentication happens in the cloud, PTA delegates authentication back to your on-premises AD in real time. When a user signs into Entra ID, the authentication request is passed through to your DC via a lightweight agent. The password is never synced to the cloud — not even a hash.
When to use PTA vs PHS:  PTA is used when organizational policy requires that passwords (even hashed) never leave on-premises. The tradeoff: if your on-prem DC or the PTA agent goes down, cloud authentication fails. PHS is more resilient. Understanding this tradeoff is a real interview question.</p>

<h3 id="step-31--switch-azure-ad-connect-from-phs-to-pta">Step 3.1 — Switch Azure AD Connect from PHS to PTA</h3>
<ul>
  <li>On VM-DC01, open Azure AD Connect from the Start Menu</li>
  <li>Click Configure → Change user sign-in → Next</li>
  <li>Enter your Entra ID Global Admin credentials</li>
  <li>Select: Pass-through authentication → Next</li>
  <li>Verify that Enable single sign-on is also checked
<img src="/assets/img/4-authentication-methods/pta-config.png" alt="pta-config" /></li>
  <li>Configure → the PTA agent is automatically installed on VM-DC01</li>
</ul>

<h3 id="step-32--verify-pta-agent-is-running">Step 3.2 — Verify PTA Agent is Running</h3>
<ul>
  <li>In Entra ID portal → Azure AD Connect → Pass-through authentication</li>
  <li>You should see VM-DC01 listed as an active agent with status: Active</li>
  <li>The agent version and last active timestamp confirm it is communicating with Entra ID</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/pta-agent.png" alt="pta-agent" /></p>

<h3 id="step-33--test-pta-authentication">Step 3.3 — Test PTA Authentication</h3>
<ul>
  <li>Open a private browser → sign into portal.azure.com as alice.smith@p2sp.onmicrosoft.com</li>
  <li>This time, Entra ID passes the authentication to VM-DC01 via the PTA agent</li>
  <li>Check Sign-in logs → authentication method now shows: Pass-through authentication</li>
  <li>To see what happens when on-prem fails: stop the Azure AD Connect PTA agent service on VM-DC01 → attempt sign-in again → it should fail</li>
  <li>Restart the agent service → verify sign-in works again</li>
</ul>

<blockquote>
  <p>⚠ Warning:  Stopping the PTA agent in production would lock out users. This is why production deployments run multiple PTA agents on different servers for redundancy.</p>
</blockquote>

<h2 id="phase-4--active-directory-federation-services-adfs">Phase 4 — Active Directory Federation Services (ADFS)</h2>

<h3 id="what-adfs-actually-does">What ADFS Actually Does</h3>
<p>ADFS is a claims-based identity solution. Instead of syncing identities or delegating password checks, ADFS issues SAML tokens that assert claims about the user (who they are, what groups they belong to, their department etc.). Relying parties (apps) trust the ADFS server and accept its tokens. Authentication happens entirely on-premises.
Why ADFS is legacy but still important:  Most large enterprises built their federation infrastructure on ADFS 10-15 years ago. Many still run it. Understanding ADFS is important for inheriting or auditing those environments. Greenfield deployments today would use Entra ID native federation instead.</p>

<h3 id="step-41--deploy-second-vm-for-adfs-vm-adfs01">Step 4.1 — Deploy Second VM for ADFS (VM-ADFS01)</h3>
<ul>
  <li>In Azure Portal → Virtual Machines → Create</li>
  <li>Name: VM-ADFS01 - Same resource group, same VNet, same subnet</li>
  <li>Image: Windows Server 2022 Datacenter - Size: Standard_B2s</li>
  <li>Username: labadmin - Same password</li>
  <li>Allow RDP inbound</li>
  <li>Create → wait for deployment</li>
  <li>After deployment: join VM-ADFS01 to lab.local domain
    <ul>
      <li>RDP into VM-ADFS01</li>
      <li>Windows + R → type <code class="language-plaintext highlighter-rouge">sysdm.cpl</code> &amp; enter → Click Change button next to “To rename this computer or change its domain” → Domain: lab.local</li>
      <li>Enter lab\labadmin credentials → reboot</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/img/4-authentication-methods/domain-change-adfs.png" alt="domain-join" /></p>

<h3 id="step-42--create-a-self-signed-certificate-for-adfs">Step 4.2 — Create a Self-Signed Certificate for ADFS</h3>
<p>ADFS requires an SSL certificate. For lab purposes we use a self-signed cert.</p>
<ul>
  <li>RDP into VM-ADFS01 as lab\labadmin</li>
  <li>Open PowerShell as Administrator and run:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$cert = New-SelfSignedCertificate -DnsName 'adfs.lab.local' -CertStoreLocation 'cert:\LocalMachine\My' -KeyLength 2048
</code></pre></div>    </div>
  </li>
  <li>Export the thumbprint for later:
<code class="language-plaintext highlighter-rouge">$cert.Thumbprint</code></li>
</ul>

<p><img src="/assets/img/4-authentication-methods/adfs-cert.png" alt="adfs-cert" /></p>

<h3 id="step-43--install-adfs-role-on-vm-adfs01">Step 4.3 — Install ADFS Role on VM-ADFS01</h3>
<ul>
  <li>Open Server Manager → Add Roles and Features</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/adfs-role.png" alt="adfs-role" /></p>

<blockquote>
  <p>This is enough for learning purposes. Converting an Entra ID domain to ADFS-federated in 2026 is a legacy operation. Microsoft’s own documentation now steers you toward migrating away from ADFS, not toward it. We can safely ignore the rest of this ADFS lab.</p>
</blockquote>

<ul>
  <li>Select: Active Directory Federation Services → Install</li>
  <li>After install, click Configure the federation service on this server</li>
  <li>Select: Create the first federation server in a federation server farm</li>
  <li>Connect with lab\labadmin domain admin credentials</li>
  <li>SSL Certificate: select the adfs.lab.local cert created above</li>
  <li>Federation service name: adfs.lab.local</li>
  <li>Federation service display name: Lab ADFS</li>
  <li>Service account: create a Group Managed Service Account (gMSA) named adfssvc</li>
  <li>Database: Use Windows Internal Database (WID) for lab — sufficient for testing</li>
  <li>Configure → Next → Configure</li>
</ul>

<h3 id="step-44--configure-entra-id-to-use-adfs-federated-domain">Step 4.4 — Configure Entra ID to use ADFS (Federated Domain)</h3>
<p>This is the most important step — converting a managed domain to a federated domain tells Entra ID to redirect authentication to your ADFS server instead of handling it natively.</p>
<ul>
  <li>On VM-DC01, install the MSOnline PowerShell module:
<code class="language-plaintext highlighter-rouge">Install-Module Microsoft.Graph -Force
Connect-MgGraph -Scopes "Domain.ReadWrite.All"
</code></li>
  <li>Enter your Entra ID Global Admin credentials when prompted</li>
  <li>Convert the domain to federated (replace with your actual domain):
Convert-MsolDomainToFederated -DomainName xxxx.onmicrosoft.com -SupportMultipleDomain</li>
  <li>Verify federation settings:
Get-MsolDomainFederationSettings -DomainName xxx.onmicrosoft.com
⚠ Warning:  Converting to a federated domain means ALL authentication for that domain goes through ADFS. If ADFS goes down, no one can sign in. Always keep at least one cloud-only Global Admin as a break-glass account that is NOT in the federated domain.</li>
</ul>

<h3 id="step-45--test-adfs-authentication-flow">Step 4.5 — Test ADFS Authentication Flow</h3>
<ul>
  <li>Open a private browser → go to portal.azure.com</li>
  <li>Enter alice.smith@xxx.onmicrosoft.com</li>
  <li>Entra ID should redirect you to the ADFS sign-in page (adfs.lab.local/adfs/ls)</li>
  <li>Sign in with AD credentials → ADFS issues a SAML token → Entra ID accepts it → access granted</li>
  <li>Check Entra ID Sign-in logs → authentication method: Federated</li>
  <li>On VM-ADFS01, open Event Viewer → Applications and Services Logs → AD FS → Admin — observe the token issuance events
💡 Tip:  The ADFS Event Viewer logs are goldmine for understanding what happens during federation. Every authentication attempt, every claim issued, every error is logged here. Get comfortable reading them.</li>
</ul>

<h2 id="phase-5--entra-cloud-sync">Phase 5 — Entra Cloud Sync</h2>

<h3 id="what-entra-cloud-sync-actually-does">What Entra Cloud Sync Actually Does</h3>
<p>Entra Cloud Sync is Microsoft’s modern, lightweight replacement for Azure AD Connect. Instead of a heavyweight on-prem application with a full sync engine, Cloud Sync uses a small provisioning agent that communicates with a sync engine hosted in Entra ID. Configuration is done entirely in the cloud portal — nothing needs to be configured on-prem beyond installing the agent.</p>

<h3 id="step-51--first-convert-domain-back-to-managed">Step 5.1 — First, Convert Domain Back to Managed</h3>
<p>If you completed Phase 4 (ADFS), your domain is federated. Convert it back to managed before testing Cloud Sync.</p>
<ul>
  <li>On VM-DC01, open PowerShell:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Connect-MsolService
Convert-MsolDomainToStandard -DomainName xxx.onmicrosoft.com -SkipUserConversion $false 
</code></pre></div>    </div>
    <p>-PasswordFile C:\temp\passwords.txt</p>
  </li>
  <li>Also switch Azure AD Connect back to PHS or disable it — Cloud Sync and Azure AD Connect cannot run simultaneously on the same domain</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/disable-connect-sync.png" alt="Disable Connect" /></p>

<h3 id="step-52--install-the-entra-cloud-sync-provisioning-agent">Step 5.2 — Install the Entra Cloud Sync Provisioning Agent</h3>
<ul>
  <li>On VM-DC01, go to Entra ID portal → Hybrid management → Azure AD Connect → Cloud sync</li>
  <li>Click: Download agent</li>
  <li>Run the installer: AADConnectProvisioningAgentSetup.exe</li>
  <li>Sign in with your Entra ID Global Admin account during install</li>
  <li>Select: Active Directory Domain Services</li>
  <li>Add lab.local domain → enter lab\labadmin credentials</li>
  <li>Finish installation</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/entra-cloud-sync.png" alt="cloud-sync" /></p>

<h3 id="step-53--create-a-cloud-sync-configuration-in-entra-id-portal">Step 5.3 — Create a Cloud Sync Configuration in Entra ID Portal</h3>
<ul>
  <li>In Entra ID portal → Azure AD Connect → Cloud sync → New configuration</li>
  <li>Select: lab.local from the dropdown</li>
  <li>Enable Password Hash Sync: Yes</li>
</ul>

<p><img src="/assets/img/4-authentication-methods/config-entra-cloud-sync.png" alt="config-entra-cloud-sync" /></p>
<ul>
  <li>Under Scope — add a scoping filter to sync only the LabUsers OU:</li>
  <li>Attribute: distinguishedName - Operator: CONTAINS - Value: OU=LabUsers,DC=lab,DC=local</li>
  <li>Review attribute mappings — observe how AD attributes map to Entra ID attributes</li>
  <li>Enable the configuration → Save</li>
  <li>Monitor the provisioning logs in the portal — you will see each user synced in real time
💡 Tip:  The scoping filter is a critical concept — in real environments you never sync all AD users to Entra ID. You sync specific OUs or groups. This is a common interview question about Cloud Sync configuration.</li>
</ul>

<h3 id="step-54--verify-and-compare-with-azure-ad-connect">Step 5.4 — Verify and Compare with Azure AD Connect</h3>
<ul>
  <li>Go to Entra ID → Users — verify LabUsers OU members appear</li>
  <li>Note: users synced via Cloud Sync show Source: Azure Active Directory (provisioning)</li>
  <li>Compare with users synced via Azure AD Connect — they show Source: Windows Server AD</li>
  <li>Review the provisioning logs: Entra ID → Monitoring → Provisioning logs — observe the sync events per user</li>
</ul>

<h2 id="what-youve-built--what-it-means">What You’ve Built &amp; What It Means</h2>

<p><img src="/assets/img/4-authentication-methods/summary.png" alt="summary" /></p>

<h3 id="cleanup--avoid-unnecessary-azure-costs">Cleanup — Avoid Unnecessary Azure Costs</h3>
<ul>
  <li>When done for the day: go to both VMs → Stop (this deallocates them, stopping compute charges)</li>
  <li>When completely done with the lab: delete rg-hybrid-lab resource group — this deletes everything inside it</li>
  <li>In Entra ID: remove the synced test users if no longer needed</li>
  <li>Revert any federated domain configuration back to managed</li>
</ul>

<h3 id="portfolio-documentation">Portfolio Documentation</h3>
<p>This lab covers the following bullet points you can legitimately add to your resume and discuss in interviews:</p>
<ul>
  <li>Deployed and configured Windows Server ADDS in Azure as a simulated on-premises domain controller</li>
  <li>Implemented hybrid identity sync using Azure AD Connect with Password Hash Sync and Pass-Through Authentication</li>
  <li>Configured ADFS for claims-based federation between on-premises AD and Entra ID — observed SAML token issuance and relying party trust</li>
  <li>Deployed Entra Cloud Sync provisioning agent with OU-scoped filtering — contrasted architecture and tradeoffs against Azure AD Connect</li>
  <li>Analyzed authentication flow differences across PHS, PTA, and ADFS using Entra ID sign-in logs</li>
</ul>]]></content><author><name>shaheerkj</name></author><category term="Authentication" /><category term="Cloud Security" /><category term="Microsoft Entra ID" /><category term="Azure AD Connect" /><category term="Hybrid Identity" /><category term="Password Hash Sync" /><category term="Pass-Through Authentication" /><category term="ADFS" /><category term="Active Directory Federation Services" /><category term="Identity and Access Management" /><category term="IAM" /><category term="Azure Active Directory" /><category term="Entra Cloud Sync" /><category term="Zero Trust" /><category term="Cloud Security" /><category term="Active Directory Windows Server" /><category term="Azure" /><category term="Microsoft 365" /><category term="SC-300" /><category term="AZ-500" /><category term="Cybersecurity" /><summary type="html"><![CDATA[A hands-on breakdown of the three hybrid identity authentication methods used in enterprise Microsoft environments — Password Hash Sync, Pass-Through Authentication, and ADFS. Covers how each method works under the hood, when organizations choose one over another, and the real security tradeoffs involved. Includes a step-by-step lab walkthrough of deploying each method using Azure VMs, Azure AD Cloud sync, and Entra ID.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.shaheerkj.me/assets/img/4-authentication-methods/cover.png" /><media:content medium="image" url="https://blog.shaheerkj.me/assets/img/4-authentication-methods/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Exploring Cloud Security Assessment &amp;amp; Posture Management tools</title><link href="https://blog.shaheerkj.me/posts/exploring-cloud-security-assessment-tools/" rel="alternate" type="text/html" title="Exploring Cloud Security Assessment &amp;amp; Posture Management tools" /><published>2026-03-10T00:00:00+00:00</published><updated>2026-04-29T18:55:50+00:00</updated><id>https://blog.shaheerkj.me/posts/exploring-cloud-security-assessment-tools</id><content type="html" xml:base="https://blog.shaheerkj.me/posts/exploring-cloud-security-assessment-tools/"><![CDATA[<p>Cloud Security Posture Management (CSPM) tools help organizations continuously monitor and assess the security posture of their cloud environments. They identify misconfigurations, compliance violations, and potential threats across cloud infrastructure.</p>

<hr />

<h2 id="what-is-cspm">What is CSPM?</h2>

<p>CSPM refers to a category of security tools that automate the identification and remediation of risks across cloud infrastructure. These tools help ensure that cloud resources are configured according to security best practices and compliance requirements.</p>

<p>Key capabilities include:</p>
<ul>
  <li><strong>Continuous monitoring</strong> of cloud resource configurations</li>
  <li><strong>Compliance assessment</strong> against frameworks (CIS, NIST, PCI-DSS, SOC 2)</li>
  <li><strong>Misconfiguration detection</strong> and remediation guidance</li>
  <li><strong>Visibility</strong> across multi-cloud environments</li>
</ul>

<hr />

<h2 id="cloud-native-cspm-tools">Cloud-Native CSPM Tools</h2>

<h3 id="microsoft-defender-for-cloud">Microsoft Defender for Cloud</h3>

<p>Microsoft Defender for Cloud is Azure’s built-in CSPM and Cloud Workload Protection Platform (CWPP). It provides a centralized dashboard to assess the security posture of your Azure, AWS, and GCP resources.</p>

<ul>
  <li><strong>What it does</strong>: Continuously evaluates your cloud resources against security benchmarks (Microsoft Cloud Security Benchmark, CIS, NIST, PCI-DSS). It generates a <strong>Secure Score</strong> that quantifies your overall posture and prioritizes remediation steps.</li>
  <li><strong>When to use it</strong>: If you’re running workloads in Azure (or multi-cloud with Azure as your primary), this is the go-to tool. It integrates natively with Azure Policy, Microsoft Sentinel, and Defender plans for VMs, containers, databases, and more.</li>
  <li><strong>Key features</strong>: Secure Score, regulatory compliance dashboard, attack path analysis, agentless scanning, and auto-remediation through Azure Policy.</li>
  <li><strong>Cost</strong>: The foundational CSPM features are <strong>free</strong>. Advanced features (Defender plans for servers, containers, databases, etc.) are paid per-resource.</li>
</ul>

<h3 id="aws-security-hub">AWS Security Hub</h3>

<p>AWS Security Hub is Amazon’s native security posture management service that aggregates findings from multiple AWS security services into a single pane of glass.</p>

<ul>
  <li><strong>What it does</strong>: Collects and normalizes findings from services like AWS Config, GuardDuty, Inspector, IAM Access Analyzer, and Firewall Manager. It runs automated compliance checks against standards like CIS AWS Foundations Benchmark, AWS Foundational Security Best Practices, and PCI-DSS.</li>
  <li><strong>When to use it</strong>: If your infrastructure is primarily on AWS. It acts as a central hub for all security findings across your AWS accounts and regions.</li>
  <li><strong>Key features</strong>: Automated compliance checks, cross-account aggregation, integration with AWS Organizations, custom actions for automated remediation via EventBridge + Lambda.</li>
  <li><strong>Cost</strong>: Priced per compliance check and per finding ingested. Free tier available for 30 days.</li>
</ul>

<h3 id="google-cloud-security-command-center">Google Cloud Security Command Center</h3>

<p>Google Cloud SCC is GCP’s native security and risk management platform that provides visibility into your Google Cloud assets and their security state.</p>

<ul>
  <li><strong>What it does</strong>: Discovers and inventories all your GCP assets, detects misconfigurations and vulnerabilities, identifies threats through integrated threat detection (Event Threat Detection, Container Threat Detection), and visualizes attack paths.</li>
  <li><strong>When to use it</strong>: If you’re running workloads on GCP. The Premium tier adds features like Security Health Analytics, compliance monitoring, and the Virtual Red Team for attack path simulation.</li>
  <li><strong>Key features</strong>: Asset inventory, vulnerability scanning, threat detection, compliance monitoring (CIS, PCI-DSS, NIST), and findings export to SIEM/SOAR tools.</li>
  <li><strong>Cost</strong>: Standard tier is <strong>free</strong> with basic asset discovery. Premium tier is paid and unlocks full CSPM capabilities.</li>
</ul>

<hr />

<h2 id="open-source--third-party-tools">Open-Source &amp; Third-Party Tools</h2>

<h3 id="prowler">Prowler</h3>

<p><a href="https://github.com/prowler-cloud/prowler">Prowler</a> is an open-source CLI security assessment tool that performs hundreds of checks across AWS, Azure, and GCP.</p>

<ul>
  <li><strong>What it does</strong>: Runs automated security audits and generates reports against compliance frameworks including CIS Benchmarks, PCI-DSS, HIPAA, GDPR, SOC 2, NIST 800-53, and more. It outputs results in multiple formats (CSV, JSON, HTML) and can send findings to AWS Security Hub.</li>
  <li><strong>When to use it</strong>: When you want a <strong>free, framework-agnostic</strong> tool to audit your cloud environments on-demand or in CI/CD pipelines. Great for multi-cloud teams that don’t want to rely solely on vendor-native tools.</li>
  <li><strong>Key features</strong>: 300+ checks for AWS, 200+ for Azure, 70+ for GCP, multi-output format, CI/CD integration, and SaaS version (Prowler Cloud) available.</li>
</ul>

<h3 id="scoutsuite">ScoutSuite</h3>

<p><a href="https://github.com/nccgroup/ScoutSuite">ScoutSuite</a> is an open-source multi-cloud security auditing tool that collects configuration data from cloud APIs and generates an interactive HTML report.</p>

<ul>
  <li><strong>What it does</strong>: Fetches resource configurations from AWS, Azure, GCP, Oracle Cloud, and Alibaba Cloud, then applies a set of rules to flag dangerous configurations. The output is a self-contained HTML report you can browse locally.</li>
  <li><strong>When to use it</strong>: When you need a <strong>quick, visual snapshot</strong> of your cloud security posture across multiple providers. Useful for security assessments and penetration tests where you want a browsable report without needing a dashboard or SaaS tool.</li>
  <li><strong>Key features</strong>: Multi-cloud support (5 providers), interactive HTML report, rule-based engine, no agents required, and easy to extend with custom rules.</li>
</ul>

<h3 id="cloudsploit">CloudSploit</h3>

<p><a href="https://github.com/aquasecurity/cloudsploit">CloudSploit</a> (now part of Aqua Security) is an open-source cloud security configuration monitoring tool.</p>

<ul>
  <li><strong>What it does</strong>: Scans AWS, Azure, GCP, and Oracle Cloud for misconfigurations and compliance violations. Checks cover areas like IAM, networking, encryption, logging, and monitoring. The open-source version runs as a CLI tool; the managed SaaS version provides continuous monitoring.</li>
  <li><strong>When to use it</strong>: When you want a lightweight, <strong>developer-friendly</strong> scanner that’s easy to integrate into automated workflows. Good for teams that want quick misconfiguration checks without heavyweight tooling.</li>
  <li><strong>Key features</strong>: Plugin-based architecture (easy to add custom checks), supports 4 cloud providers, JSON output, and a SaaS offering through Aqua Security.</li>
</ul>

<h3 id="checkov">Checkov</h3>

<p>Checkov is a static analysis tool for Infrastructure-as-Code (IaC) that catches security misconfigurations <strong>before</strong> resources are deployed.</p>

<ul>
  <li><strong>What it does</strong>: Scans Terraform, CloudFormation, Kubernetes manifests, Helm charts, ARM templates, Bicep, Serverless Framework, and Dockerfile files for security and compliance issues. It also supports scanning runtime cloud configurations via integration with Bridgecrew.</li>
  <li><strong>When to use it</strong>: In your <strong>CI/CD pipeline</strong> to shift security left. Run it before <code class="language-plaintext highlighter-rouge">terraform apply</code> or <code class="language-plaintext highlighter-rouge">az deployment create</code> to catch problems at the code stage rather than in production. Essential for GitOps and IaC-heavy workflows.</li>
  <li><strong>Key features</strong>: 1000+ built-in policies, custom policy support (Python and YAML), graph-based analysis for cross-resource checks, IDE plugins, and CI/CD integration (GitHub Actions, GitLab CI, Jenkins).</li>
</ul>

<h3 id="azure-quick-review-azqr">Azure Quick Review (azqr)</h3>

<p><a href="https://github.com/Azure/azqr">Azure Quick Review</a> (azqr) is an open-source CLI tool from Microsoft that scans Azure resources and produces a detailed Excel report on best practice recommendations.</p>

<ul>
  <li><strong>What it does</strong>: Evaluates your Azure resources against the Azure Well-Architected Framework pillars (Reliability, Security, Cost Optimization, Operational Excellence, Performance Efficiency). It checks for things like missing diagnostic settings, lack of availability zones, unencrypted resources, and missing private endpoints.</li>
  <li><strong>When to use it</strong>: When you want a <strong>quick, one-shot assessment</strong> of your Azure subscription before an architecture review or audit. It’s lightweight (single binary, no install) and produces a spreadsheet that’s easy to share with stakeholders.</li>
  <li><strong>Key features</strong>: Covers 80+ Azure resource types, Excel output with categorized findings, Azure Well-Architected alignment, no authentication token required beyond Azure CLI login, and runs in minutes.</li>
</ul>

<h3 id="threatmapper">ThreatMapper</h3>

<p><a href="https://github.com/deepfence/ThreatMapper">ThreatMapper</a> by Deepfence is an open-source cloud-native threat detection and attack surface management platform.</p>

<ul>
  <li><strong>What it does</strong>: Goes beyond configuration checks — it discovers running workloads (VMs, containers, serverless), scans them for vulnerabilities (CVEs), detects exposed secrets, and maps the <strong>attack surface</strong> by correlating findings with network reachability. It visualizes threat paths from the internet to your sensitive assets.</li>
  <li><strong>When to use it</strong>: When you want <strong>runtime visibility</strong> into your cloud-native workloads, not just configuration audits. Ideal for organizations running Kubernetes, Docker, or serverless at scale that need to understand which vulnerabilities are actually exploitable based on network exposure.</li>
  <li><strong>Key features</strong>: Topology visualization of cloud assets, runtime vulnerability scanning, secret scanning, malware detection, attack path mapping, Kubernetes and Docker support, integrations with Slack/Jira/SIEM, and a management console UI.</li>
</ul>

<h3 id="scubagear">ScubaGear</h3>

<p><a href="https://github.com/cisagov/ScubaGear">ScubaGear</a> (Secure Cloud Business Applications Gear) is a tool developed by <strong>CISA (Cybersecurity and Infrastructure Security Agency)</strong> to assess the security configuration of Microsoft 365 (M365) tenants.</p>

<ul>
  <li><strong>What it does</strong>: Evaluates your M365 tenant against CISA’s Secure Cloud Business Applications (SCuBA) security baselines. It checks configurations for <strong>Azure Active Directory / Entra ID, Exchange Online, SharePoint Online, OneDrive, Microsoft Teams, Power BI, Power Platform, and Defender for Office 365</strong>.</li>
  <li><strong>When to use it</strong>: When you need to audit your <strong>Microsoft 365 security posture</strong> against US government-recommended baselines. Essential for government agencies (required by CISA BOD 25-01) and any organization that wants to harden their M365 configuration.</li>
  <li><strong>Key features</strong>: PowerShell-based (runs locally), generates HTML reports with pass/fail results per baseline, covers 8 M365 products, regularly updated baselines, and fully open-source.</li>
</ul>

<h3 id="maester">Maester</h3>

<p><a href="https://maester.dev">Maester</a> is an open-source <strong>test automation framework</strong> for verifying Microsoft Entra ID (Azure AD), Microsoft 365, and Azure security configurations using Pester (PowerShell testing framework).</p>

<ul>
  <li><strong>What it does</strong>: Lets you define security configuration baselines as Pester tests and run them against your tenant. It ships with a growing library of built-in tests covering Entra ID Conditional Access policies, authentication methods, privileged roles, M365 settings, and more. Think of it as <strong>unit tests for your identity and M365 security settings</strong>.</li>
  <li><strong>When to use it</strong>: When you want <strong>continuous, automated validation</strong> that your Entra ID and M365 configurations haven’t drifted from your desired security baseline. Ideal for DevOps/SecOps teams that want to integrate identity security checks into CI/CD pipelines or scheduled monitoring.</li>
  <li><strong>Key features</strong>: Pester-based (familiar to PowerShell users), built-in test library aligned with CIS and CISA recommendations, custom test support, HTML and NUnit report output, GitHub Actions integration, and daily scheduled monitoring support.</li>
</ul>

<hr />

<h2 id="comparison">Comparison</h2>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>Defender for Cloud</th>
      <th>AWS Security Hub</th>
      <th>Prowler</th>
      <th>ScoutSuite</th>
      <th>Checkov</th>
      <th>azqr</th>
      <th>ThreatMapper</th>
      <th>ScubaGear</th>
      <th>Maester</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Focus area</strong></td>
      <td>Cloud CSPM + CWPP</td>
      <td>AWS security aggregation</td>
      <td>Multi-cloud audit</td>
      <td>Multi-cloud audit</td>
      <td>IaC scanning</td>
      <td>Azure best practices</td>
      <td>Runtime threat detection</td>
      <td>M365 security baselines</td>
      <td>Entra ID / M365 testing</td>
    </tr>
    <tr>
      <td><strong>Multi-cloud</strong></td>
      <td>Azure, AWS, GCP</td>
      <td>AWS only</td>
      <td>AWS, Azure, GCP</td>
      <td>AWS, Azure, GCP, OCI, Alibaba</td>
      <td>Multi-IaC (Terraform, ARM, CFN, K8s)</td>
      <td>Azure only</td>
      <td>Any cloud (runtime)</td>
      <td>M365 only</td>
      <td>Entra ID / M365</td>
    </tr>
    <tr>
      <td><strong>Compliance frameworks</strong></td>
      <td>CIS, NIST, PCI-DSS, SOC 2</td>
      <td>CIS, AWS FSBP, PCI-DSS</td>
      <td>CIS, PCI, HIPAA, NIST, SOC 2</td>
      <td>Custom rules</td>
      <td>1000+ built-in policies</td>
      <td>Azure Well-Architected</td>
      <td>CVE-based</td>
      <td>CISA SCuBA baselines</td>
      <td>CIS, CISA baselines</td>
    </tr>
    <tr>
      <td><strong>Auto-remediation</strong></td>
      <td>Yes (Azure Policy)</td>
      <td>Yes (EventBridge + Lambda)</td>
      <td>No (reporting only)</td>
      <td>No</td>
      <td>No (pre-deploy prevention)</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
    </tr>
    <tr>
      <td><strong>Output</strong></td>
      <td>Portal dashboard</td>
      <td>Console dashboard</td>
      <td>CSV, JSON, HTML</td>
      <td>Interactive HTML</td>
      <td>CLI, JSON, SARIF</td>
      <td>Excel report</td>
      <td>Web console</td>
      <td>HTML report</td>
      <td>HTML, NUnit</td>
    </tr>
    <tr>
      <td><strong>Cost</strong></td>
      <td>Free tier + paid plans</td>
      <td>Per-check pricing</td>
      <td>Free (OSS)</td>
      <td>Free (OSS)</td>
      <td>Free (OSS)</td>
      <td>Free (OSS)</td>
      <td>Free (OSS)</td>
      <td>Free (OSS)</td>
      <td>Free (OSS)</td>
    </tr>
    <tr>
      <td><strong>Best for</strong></td>
      <td>Azure-first orgs</td>
      <td>AWS-first orgs</td>
      <td>Multi-cloud audits</td>
      <td>Quick visual assessments</td>
      <td>Shift-left IaC security</td>
      <td>Azure architecture reviews</td>
      <td>Runtime attack surface</td>
      <td>M365 tenant hardening</td>
      <td>Identity security CI/CD</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="choosing-the-right-tool">Choosing the Right Tool</h2>

<p>There’s no single tool that covers everything. The best approach is to <strong>layer tools</strong> based on your needs:</p>

<ul>
  <li><strong>Cloud-native CSPM</strong> (Defender for Cloud / Security Hub / SCC) for continuous posture management in your primary cloud provider.</li>
  <li><strong>Prowler or ScoutSuite</strong> for on-demand multi-cloud audits and compliance checks.</li>
  <li><strong>Checkov</strong> in your CI/CD pipeline to catch IaC misconfigurations before deployment.</li>
  <li><strong>azqr</strong> for Azure Well-Architected reviews and quick subscription assessments.</li>
  <li><strong>ThreatMapper</strong> for runtime vulnerability and attack surface visibility in container/Kubernetes environments.</li>
  <li><strong>ScubaGear</strong> to validate your Microsoft 365 tenant against CISA baselines.</li>
  <li><strong>Maester</strong> for continuous, automated Entra ID and M365 security testing in your pipelines.</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Cloud security isn’t a one-and-done activity — it’s an ongoing process that scales with your infrastructure. The tools covered in this post range from cloud-native platforms like Defender for Cloud and AWS Security Hub that give you continuous visibility, to open-source scanners like Prowler and Checkov that fit cleanly into DevOps workflows, to specialized tools like ScubaGear and Maester that lock down your Microsoft 365 and Entra ID configurations.</p>

<p>No single tool will catch everything. The real value comes from combining them: use cloud-native CSPM for day-to-day monitoring, IaC scanners to shift left, and targeted tools like azqr or ScubaGear for periodic deep-dive assessments. Most of the open-source options covered here are free and can be up and running in minutes, so there’s very little barrier to getting started.</p>

<p>The most important step is the first one — pick a tool, run a scan, and start closing gaps.</p>]]></content><author><name>shaheerkj</name></author><category term="Cloud Security" /><category term="Compliance" /><category term="CSPM" /><category term="security assessment" /><category term="Prowler" /><category term="Checkov" /><category term="ScubaGear" /><category term="Maester" /><category term="azqr" /><category term="ThreatMapper" /><category term="Defender for Cloud" /><category term="cloud compliance" /><summary type="html"><![CDATA[A comprehensive guide to CSPM tools like Prowler, Checkov, ScubaGear, Maester, azqr, and ThreatMapper for cloud security assessment and compliance auditing across Azure, AWS, and GCP.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.shaheerkj.me/assets/img/2-cspm/image.png" /><media:content medium="image" url="https://blog.shaheerkj.me/assets/img/2-cspm/image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Detecting Azure Workload Misconfigurations Using Azure Quick Review (azqr)</title><link href="https://blog.shaheerkj.me/posts/using-azqr-to-detect-azure-misconfigurations/" rel="alternate" type="text/html" title="Detecting Azure Workload Misconfigurations Using Azure Quick Review (azqr)" /><published>2026-03-10T00:00:00+00:00</published><updated>2026-04-29T18:55:50+00:00</updated><id>https://blog.shaheerkj.me/posts/using-azqr-to-detect-azure-misconfigurations</id><content type="html" xml:base="https://blog.shaheerkj.me/posts/using-azqr-to-detect-azure-misconfigurations/"><![CDATA[<p>In this lab, I explore how Azure Quick Review (azqr)—a Microsoft tool—can be used to assess Azure workloads for misconfigurations.</p>

<p>As I discussed in my <a href="/posts/exploring-cloud-security-assessment-tools/#azure-quick-review-azqr">previous post</a>, azqr is one of several tools organizations can use to gain visibility into the security posture of their workloads and resources.</p>

<hr />

<h3 id="lab-setup">Lab Setup</h3>

<p>The vulnerable infrastructure was deployed using Terraform. The source code is available <a href="https://github.com/shaheerkj/vulnerable-azure-lab">here</a>.</p>

<p>Prerequisites are listed in the GitHub repo. The repo was originally built to support <a href="/posts/exploring-cloud-security-assessment-tools/#prowler">Prowler</a>, but it works equally well for testing azqr’s capabilities.</p>

<p>After cloning the repo and provisioning the infrastructure with Terraform (instructions are in the GitHub repo), the next step is to install azqr.</p>

<p>From the official repo, there are two ways to install it:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">winget install azqr</code></li>
</ul>

<p>OR</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-ExecutionPolicy</span><span class="w"> </span><span class="nx">Bypass</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">Process</span><span class="w"> </span><span class="nt">-Force</span><span class="p">;</span><span class="w"> </span><span class="p">[</span><span class="n">System.Net.ServicePointManager</span><span class="p">]::</span><span class="n">SecurityProtocol</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">System.Net.ServicePointManager</span><span class="p">]::</span><span class="n">SecurityProtocol</span><span class="w"> </span><span class="o">-bor</span><span class="w"> </span><span class="nx">3072</span><span class="p">;</span><span class="w"> </span><span class="n">iex</span><span class="w"> </span><span class="p">((</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadString</span><span class="p">(</span><span class="s1">'https://raw.githubusercontent.com/azure/azqr/main/scripts/install.ps1'</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p>Since I was using PowerShell at the time, I went with the latter option. This one-liner downloads and runs the azqr install script directly from GitHub.</p>

<p>The script drops the <code class="language-plaintext highlighter-rouge">azqr.exe</code> executable in the current working directory. I moved it to a dedicated <code class="language-plaintext highlighter-rouge">Azqr/</code> folder inside <code class="language-plaintext highlighter-rouge">Program Files/</code> for easier access.</p>

<hr />

<h3 id="running-azqr">Running azqr</h3>

<p>Now that azqr is installed, let’s look at a few of its flags and options:</p>

<p><img src="/assets/img/3-azqr/azqr-help.png" alt="Azqr Help" /></p>

<p>The option we’re interested in is <code class="language-plaintext highlighter-rouge">scan</code>, which allows us to scan a subscription for misconfigurations.</p>

<p>Syntax for the scan command:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">azqr</span><span class="w"> </span><span class="nx">scan</span><span class="w"> </span><span class="nt">-s</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">SUB_ID</span><span class="err">&gt;</span><span class="w">
</span></code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">scan</code> command offers several additional flags and options, the most notable being:</p>

<ol>
  <li>Scanning specific resources only.</li>
  <li><code class="language-plaintext highlighter-rouge">--filter</code> to apply a filter (in YAML format).</li>
  <li><code class="language-plaintext highlighter-rouge">-g</code> to scope the scan to a specific resource group.</li>
  <li><code class="language-plaintext highlighter-rouge">--xlsx</code>, <code class="language-plaintext highlighter-rouge">--csv</code>, <code class="language-plaintext highlighter-rouge">--stdout</code> for different output formats.</li>
</ol>

<p>Running a scan with just the <code class="language-plaintext highlighter-rouge">-s &lt;SUB&gt;</code> flag produces an <code class="language-plaintext highlighter-rouge">.xlsx</code> file containing all findings. For example:</p>

<p><img src="/assets/img/3-azqr/scan-result.png" alt="Scan Result" /></p>

<h3 id="limitations-of-azqr">Limitations of azqr</h3>

<p>While azqr is an excellent assessment tool for Azure workloads, its output formats are not particularly user-friendly.</p>

<p>Part of my motivation for documenting azqr was to understand its inner workings, with the goal of building a more readable and accessible front-end. The idea is that users interact through a web UI, which in turn invokes the CLI to initiate scans and present results in a cleaner format.</p>]]></content><author><name>shaheerkj</name></author><category term="Cloud Security" /><category term="Compliance" /><category term="CSPM" /><category term="security assessment" /><category term="azqr" /><category term="Defender for Cloud" /><category term="Cloud Compliance" /><category term="Cloud Security" /><summary type="html"><![CDATA[A hands-on lab using azqr to scan an intentionally vulnerable Azure infrastructure for misconfigurations]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.shaheerkj.me/assets/img/3-azqr/cover.png" /><media:content medium="image" url="https://blog.shaheerkj.me/assets/img/3-azqr/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Identity &amp;amp; Authentication Protocols</title><link href="https://blog.shaheerkj.me/posts/identity-authentication-protocols/" rel="alternate" type="text/html" title="Identity &amp;amp; Authentication Protocols" /><published>2026-02-25T00:00:00+00:00</published><updated>2026-04-29T18:55:50+00:00</updated><id>https://blog.shaheerkj.me/posts/identity-authentication-protocols</id><content type="html" xml:base="https://blog.shaheerkj.me/posts/identity-authentication-protocols/"><![CDATA[<h2 id="the-big-picture">The Big Picture:</h2>

<p>All of these answer two questions:</p>
<ol>
  <li>Who is the user?</li>
  <li>What are they allowed to do?</li>
</ol>

<h2 id="saml-security-assertion-markup-language"><strong>SAML (Security Assertion Markup Language)</strong></h2>

<p>SAML is an older, XML-based standard that predates mobile/API-first architecures and many enterprises still use it. It makes this possible by providing a way to authenticate a user once and then communicate that authentication to multiple applications. The most current version in <strong>SAML 2.0</strong>.</p>

<h3 id="saml-workflow">SAML workflow:</h3>

<p>A typical SSO authentication process involves three parties</p>
<ul>
  <li><strong>Principal (also known as the “subject”)</strong>: Almost always a human user who is trying to access cloud-hosted application.</li>
  <li><strong>Identity Provider</strong>: IdP is a cloud software service that stores and confirms user identity, typically through a login process. IdP’s role is to say “I know this person and this is the list of things they are allowed to do”. An SSO system may in fact be separate from the IdP, but in those cases the SSO essentially acts as a representative for the IdP, so for all intents and purposes, they are the same in a SAML workflow.</li>
  <li><strong>Service Provider</strong>: This is the cloud-hosted application or service the user wants to use. Common examples include <strong>Gmail, M365</strong> and others include Salesforce, ServiceNow, Jira etc. Ordinarily a user would just log in to these services directly, but when SSO is used, the user logs into the SSO instead, and SAML is used to give them access instead of a direct login.</li>
</ul>

<p>Typical Workflow:</p>

<p><img src="/assets/img/identity-authentication/saml.png" alt="SAML Workflow" /></p>

<p>Working:</p>
<ul>
  <li>Uses XML-based messages</li>
  <li>Browser redirects user to an Identity Provider (IdP)</li>
  <li>IdP sends back a SAML assertion</li>
</ul>

<h2 id="oauth-20-framework"><strong>OAuth 2.0 Framework</strong>:</h2>

<p>It is a security standard where you give one application permission to access your data from another service/application.</p>

<p><img src="/assets/img/identity-authentication/oauth.png" alt="OAuth 2.0 Workflow" /></p>

<p>Components of OAuth2.0:</p>
<ol>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Resource Owner</code>: The owner of the data, you. You are in charge of your data and the things that can be done with it. You can grant permission for it.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Client</code>: The application that wants to access data or perform actions on behalf of the resource owner.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Authorization Server</code>: The server that knows the resource owner and the resource owner has an account with the authorization server.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Resource Server</code>: The API or service that the client wants to use on behalf of the user/resource owner.</p>
  </li>
</ol>

<blockquote>
  <p>Sometimes the authorization server and the resource server are the same server. However, there are cases where the two servers are different. For example, the authorization server may be a 3rd party service that the resource server trusts for validation and authorization.</p>
</blockquote>

<ol>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Redirect URI</code>: The URI that the resource server will redirect the resource owner to after granting permission to the client.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Response Type</code>: The type of information that the client expects to receive. The most common type is <code class="language-plaintext highlighter-rouge">authorization code</code>.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Scope</code>: The granualar permissions that are required by the clients. For example:</p>
  </li>
</ol>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Read contacts</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Create Contact</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Delete contact</li>
</ul>

<ol>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Consent</code>: The authorization server takes the scope that the client is requesting and verifies with the resource owner whether or not they want to give the client the requested permissions.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Client ID</code>: This ID is used to identify the client with the authorization server.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Client Secret</code>: Only the client and auth server know this, and this allows them to securely share information.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Authorization Code</code>: A short lived temporary code that the auth server sends back to the client. The client sends this <strong>Authorization code</strong> along with the <strong>Client Secret</strong> &amp; <strong>Client ID</strong> back to the authorization server in exchange for an <strong>Access token</strong>.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">Access token</code>: Is a value/key that the client will use from that point forward to communicate with the resource server.</p>
  </li>
</ol>

<h3 id="full-end-to-end-flow">Full end-to-end flow:</h3>

<ol>
  <li>
    <h5 id="user-clicks-sign-in-with-microsoft">User clicks “Sign in with Microsoft”:</h5>
    <ul>
      <li>Front-end redirects the user’s browser to entra’s authorization endpoint
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?client_id=YOUR_CLIENT_ID
&amp;response_type=code
&amp;redirect_uri=https://yourapp.com/auth/callback
&amp;scope=openid profile email
&amp;state=random_string
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>
    <h5 id="user-authenticates-with-microsoft">User authenticates with Microsoft:</h5>
  </li>
  <li>
    <h5 id="entra-id-redirects-back-to-your-app">Entra ID redirects back to your app:</h5>
    <ul>
      <li>After successful login, Entra Redirects the browser back to the redirect URI with a short-lived authorization code:
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> https://yourapp.com/auth/callback?code=AUTHORIZATION_CODE&amp;state=random_string
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>
    <h5 id="your-backend-exchanges-the-code-for-tokens">Your backend exchanges the code for tokens:</h5>
    <ul>
      <li>Backend makes a POST req to entra id:
 ```
 POST https://login.microsoftonline.com/common/oauth2/v2.0/token</li>
    </ul>

    <p>client_id=YOUR_CLIENT_ID
 client_secret=YOUR_CLIENT_SECRET
 code=AUTHORIZATION_CODE
 redirect_uri=https://yourapp.com/auth/callback
 grant_type=authorization_code
 ```
 This is the only step where your client secret is used. Entra ID verifies that the secret matches the client ID, confirming the request is genuinely from your app and not someone who intercepted the code.</p>
  </li>
  <li>
    <h5 id="entra-id-returns-tokens">Entra ID returns tokens:</h5>

    <ul>
      <li>Entra reponds with:
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> {
 "access_token": "eyJ...",
 "id_token": "eyJ...",
 "refresh_token": "eyJ...",
 "expires_in": 3600
 }
</code></pre></div>        </div>
      </li>
      <li><code class="language-plaintext highlighter-rouge">ID token</code> — contains the user’s identity (name, email, Entra object ID). This is what you use to know who just logged in.</li>
      <li><code class="language-plaintext highlighter-rouge">Access token</code> — used to call Microsoft APIs on behalf of the user (e.g. Microsoft Graph), if your app needs that.</li>
      <li><code class="language-plaintext highlighter-rouge">Refresh token</code> — lets your backend silently get new tokens when the access token expires, without making the user log in again.</li>
    </ul>
  </li>
  <li>
    <h5 id="backend-reads-the-id-token">Backend reads the ID token:</h5>

    <ul>
      <li>You decode the ID token (it’s a JWT) and extract the user’s information:
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> {
 "oid": "user's unique Entra object ID",
 "email": "user@outlook.com",
 "name": "John Doe",
 "iss": "https://login.microsoftonline.com/...",
 "aud": "YOUR_CLIENT_ID",
 "exp": 1234567890
 }
</code></pre></div>        </div>
        <p>You should validate the token — check that <code class="language-plaintext highlighter-rouge">iss</code> is Microsoft, <code class="language-plaintext highlighter-rouge">aud</code> matches your client ID, and <code class="language-plaintext highlighter-rouge">exp</code> hasn’t passed. Never skip validation.</p>
      </li>
    </ul>

    <p>The <code class="language-plaintext highlighter-rouge">oid</code> claim is the most important one — it’s the user’s permanent, unique identifier in Entra ID. Use this as their ID in your database, not their email (emails can change).</p>
  </li>
</ol>

<h2 id="openid-connect-oidc">OpenID connect (OIDC):</h2>

<p>OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. If OAuth 2.0 answers <strong>“what is this app allowed to do on the user’s behalf?”</strong>, OIDC answers <strong>“who is this user?”</strong> in a standard, interoperable way.</p>

<h3 id="how-oidc-extends-oauth-20">How OIDC extends OAuth 2.0</h3>

<ul>
  <li><strong>Reuses OAuth 2.0 flows</strong>: Authorization code flow, access tokens, refresh tokens, redirect URIs, scopes, etc.</li>
  <li><strong>Adds an <code class="language-plaintext highlighter-rouge">id_token</code></strong>: A signed JWT that the client validates to prove the user’s identity.</li>
  <li><strong>Defines standard scopes and claims</strong>: Scopes like <code class="language-plaintext highlighter-rouge">openid</code>, <code class="language-plaintext highlighter-rouge">profile</code>, <code class="language-plaintext highlighter-rouge">email</code> and well-known claims (<code class="language-plaintext highlighter-rouge">sub</code>, <code class="language-plaintext highlighter-rouge">email</code>, <code class="language-plaintext highlighter-rouge">name</code>, etc.) so different identity providers expose user info in a common shape.</li>
  <li><strong>Standardizes discovery</strong>: Via the <code class="language-plaintext highlighter-rouge">/.well-known/openid-configuration</code> endpoint so clients can auto-discover the correct authorization, token, and JWKS endpoints.</li>
</ul>

<p>In the Microsoft Entra example above, the moment you added the <code class="language-plaintext highlighter-rouge">openid profile email</code> scopes and received an <code class="language-plaintext highlighter-rouge">id_token</code>, you moved from <strong>plain OAuth 2.0</strong> into <strong>OpenID Connect</strong>:</p>

<ul>
  <li>The <strong>access token</strong> is for calling APIs on behalf of the user (authorization).</li>
  <li>The <strong>ID token</strong> is for logging the user into your app and answering “who just signed in?” (authentication).</li>
</ul>

<p>OIDC is what powers most modern <strong>“Sign in with X”</strong> buttons (Microsoft, Google, GitHub, etc.) and is generally the preferred choice for new web and mobile applications, because it gives you a clean, token-based answer to both questions from the big picture section:</p>

<ol>
  <li>Who is the user? → From the <code class="language-plaintext highlighter-rouge">id_token</code> and its claims.</li>
  <li>What are they allowed to do? → From scopes and access tokens (via OAuth 2.0).</li>
</ol>

<h2 id="comparing-saml-oauth-20-and-oidc">Comparing SAML, OAuth 2.0 and OIDC</h2>

<ul>
  <li><strong>Primary goal</strong>
    <ul>
      <li><strong>SAML</strong>: Enterprise SSO between a corporate identity provider and browser-based applications.</li>
      <li><strong>OAuth 2.0</strong>: Delegated authorization – let an application call APIs on behalf of a user.</li>
      <li><strong>OIDC</strong>: Authentication – log the user in and give the app a verified identity for them.</li>
    </ul>
  </li>
  <li><strong>Token / data format</strong>
    <ul>
      <li><strong>SAML</strong>: XML-based assertions passed via browser redirects or POSTs.</li>
      <li><strong>OAuth 2.0</strong>: Access tokens (opaque or JWT) used to protect APIs.</li>
      <li><strong>OIDC</strong>: JWT ID tokens (plus OAuth 2.0 access tokens when you also call APIs).</li>
    </ul>
  </li>
  <li><strong>Best suited for</strong>
    <ul>
      <li><strong>SAML</strong>: Older/SaaS apps in enterprise SSO ecosystems (Okta, legacy ADFS-style setups).</li>
      <li><strong>OAuth 2.0</strong>: Securing APIs, microservices, and third‑party integrations.</li>
      <li><strong>OIDC</strong>: User sign‑in flows for modern web, SPA, and mobile applications.</li>
    </ul>
  </li>
</ul>

<p>A helpful way to remember it:</p>

<ul>
  <li><strong>SAML and OIDC</strong> are mainly about <strong>“who is the user?”</strong> (authentication and SSO).</li>
  <li><strong>OAuth 2.0</strong> is mainly about <strong>“what is this app allowed to do?”</strong> (authorization against APIs and resources).</li>
</ul>

<p>In many real systems you will use OAuth 2.0 and OIDC together for a complete identity story: OIDC to sign the user in and establish who they are, and OAuth 2.0 scopes and access tokens to control what your services and APIs are allowed to do on their behalf.</p>]]></content><author><name>shaheerkj</name></author><category term="Security" /><category term="Cloud Security" /><category term="authentication" /><category term="IAM" /><category term="Entra ID" /><category term="Identity Management" /><summary type="html"><![CDATA[This post explores the identity and authentication protocol that support modern day web infrastructure]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.shaheerkj.me/assets/img/identity-authentication/cover.png" /><media:content medium="image" url="https://blog.shaheerkj.me/assets/img/identity-authentication/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>