<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mason Parle</title>
    <description>The latest articles on DEV Community by Mason Parle (@parlesec).</description>
    <link>https://dev.to/parlesec</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3727297%2F6b47cedc-73c1-4190-912f-612f1f1b0a2e.jpeg</url>
      <title>DEV Community: Mason Parle</title>
      <link>https://dev.to/parlesec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/parlesec"/>
    <language>en</language>
    <item>
      <title>Shared Signals Framework: Bringing Standards to Continuous Session Protection</title>
      <dc:creator>Mason Parle</dc:creator>
      <pubDate>Wed, 11 Feb 2026 12:09:45 +0000</pubDate>
      <link>https://dev.to/parlesec/shared-signals-framework-bringing-standards-to-continuous-session-protection-547g</link>
      <guid>https://dev.to/parlesec/shared-signals-framework-bringing-standards-to-continuous-session-protection-547g</guid>
      <description>&lt;h2&gt;
  
  
  Identity Doesn’t End at Login: The Case for In-Session Management
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;An interactive view of implemented SSF flows is available at &lt;strong&gt;&lt;a href="https://protocolsoup.com/ssf-sandbox" rel="noopener noreferrer"&gt;ProtocolSoup SSF Sandbox&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Identity has pioneered security for how users prove who they are and what they have access to. SAML, OAuth, OIDC - authentication, authorization, scopes and claims can only make it so far, and that limit is the active session. &lt;/p&gt;

&lt;p&gt;Once a user proves who (or what) they are, the onus goes back to the provider to identify the target account, scope the permission and issue an active session. &lt;/p&gt;

&lt;p&gt;Authentication is the bouncer at the door. Once you have been let in, who is in charge of keeping you safe? &lt;/p&gt;

&lt;h2&gt;
  
  
  The Static Session Problem
&lt;/h2&gt;

&lt;p&gt;Traditional identity flows are designed to follow the same general pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User authenticates&lt;/li&gt;
&lt;li&gt;IdP issues a session or access token&lt;/li&gt;
&lt;li&gt;Trust is established through the access token and maintained from a refresh token&lt;/li&gt;
&lt;li&gt;Session ends&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Between steps 3 and 4, the application has a blind spot. Traditional mechanisms of session risk are alert-based - impossible travel, suspicious IP, protected actions are dependent on session generation or inherently 'risky' behaviours. &lt;/p&gt;

&lt;p&gt;This creates problems that authentication fundamentally cannot solve. &lt;/p&gt;

&lt;p&gt;Traditional identity operates on an alert-based model. A risk is detected (credential compromise, suspicious login, compliance violation) and it generates an alert. That alert lands in a SIEM, a SOC or a workflow, an alert is raised, and there is latency for a person or process to ingest this. In the meantime, the affected sessions continue as if nothing happened.&lt;/p&gt;

&lt;p&gt;A user authenticates cleanly at 9am. Their password appears in a breach dump at 10am. The application won't know until the session expires or the user re-authenticates.&lt;/p&gt;

&lt;p&gt;This is the everyday reality of a model where risk is detected in one place and enforcement is stuck somewhere else, separated by time and manual process. The session sits in between, unaware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alert-Based vs Signal-Based: A Shift in How Risk Travels
&lt;/h2&gt;

&lt;p&gt;This alert-based model treats identity events as things humans respond to. While heuristics attempt to quantify anomalies, the great consistency of people is that they are unpredictable by nature. Every day is a new one for us. &lt;/p&gt;

&lt;p&gt;The response needs to be systemic. If the IdP knows a credential is compromised, every application relying on that credential should know too - immediately, automatically, without a human in the loop.&lt;/p&gt;

&lt;p&gt;This is the shift from alert-based contextual risk to &lt;strong&gt;signal-based progressive risk&lt;/strong&gt;. Instead of detecting risk and creating an alert for someone to triage, the system emits a signal that propagates across every relying party in real time. The response is not a ticket, it's an automatic policy re-evaluation at every application that can reach into any active session.&lt;/p&gt;

&lt;p&gt;The difference is an architectural paradigm shift. Alert-based systems have a detection layer and a disconnected enforcement layer. Signal-based systems unify them. The signal carries enough context for every receiver to independently decide what to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSF: The Protocol That Makes Sessions Dynamic
&lt;/h2&gt;

&lt;p&gt;The Shared Signals Framework (SSF) is the OpenID specification that enables the unification of risk signal generation, transmission, ingestion and automated action. It defines how identity providers and relying parties share security events in real time using Security Event Tokens (SETs). These are signed JWTs that carry structured event data correlating to a specific signal.&lt;/p&gt;

&lt;p&gt;SSF operates through two profiles:&lt;br&gt;
&lt;strong&gt;CAEP (Continuous Access Evaluation Profile)&lt;/strong&gt; handles in-session events. Actions, events, indicators that change the security context of an active session. &lt;br&gt;
CAEP solves for the session revocation, credential change, token claims update, device compliance, assurance level downgrade. These are the signals that transform sessions from static to dynamic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RISC (Risk Incident Sharing and Coordination)&lt;/strong&gt; handles account-level events. Credential compromise, account disabled, identifier changed. These are broader signals about the user's overall security posture and account standing rather than a specific session.&lt;/p&gt;

&lt;p&gt;Together, they cover the full lifecycle: CAEP manages what happens &lt;em&gt;during a session&lt;/em&gt;, RISC manages what happens &lt;em&gt;to the account&lt;/em&gt; underneath it.&lt;/p&gt;

&lt;p&gt;Going back to the bouncer… we might have gotten past with an ID that says “Alice” and looks close enough, but now there is a roaming guard that can raise a signal and boot me out if I start responding to only “Bob” instead. &lt;/p&gt;
&lt;h3&gt;
  
  
  How it works mechanically
&lt;/h3&gt;

&lt;p&gt;An SSF deployment has two roles: a &lt;strong&gt;Transmitter&lt;/strong&gt; (usually the IdP or some source of truth) and one or more &lt;strong&gt;Receivers&lt;/strong&gt; (relying parties / applications).&lt;/p&gt;

&lt;p&gt;The Transmitter detects a security-relevant event such as an admin revoking a user's session. This action is represented as a Security Event Token (SET):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://idp.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://app.example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1707400000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jti"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event-unique-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"https://schemas.openid.net/secevent/caep/event-type/session-revoked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"event_timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1707400000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"initiating_entity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason_admin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Security policy violation detected"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This SET is signed and delivered to every subscribed Receiver. It is transmitted through either a push delivery (RFC 8935) or polled by the Receiver (RFC 8936). The Receiver validates the signature against the Transmitter's JWKS, parses the event, and acts based on the event. These events are configurable by the receiver and can include actions including terminate sessions, revoke tokens, force re-authentication, trigger step-up auth…&lt;/p&gt;

&lt;p&gt;The whole cycle from detection to enforcement happens without human intervention. Alerts are raised, communicated and actioned between services and machines, no Human-In-The-Loop is required.&lt;/p&gt;

&lt;p&gt;Now that bouncer becomes a personal bodyguard, with a live intel feed transmitting any risk signals, and receiving them for immediate action. The catch is you are never completely trusted, one bad signal means you have to prove who you are once again. &lt;/p&gt;

&lt;h2&gt;
  
  
  SSF and the Attacker Mindset
&lt;/h2&gt;

&lt;p&gt;Attackers are incredibly successful. Tiny holes in an attack surface, actively exploited zero-day vulnerabilities, undetected advanced persistent threats all pose detrimental impacts that often leave practitioners feeling as though there is forever one extra control and one extra point of friction that needs to be addressed.&lt;/p&gt;

&lt;p&gt;While this may be true, it is important to consider the success of these black-hat and APT groups, and that success often comes down to sharing and collaboration.&lt;/p&gt;

&lt;p&gt;This is what Shared Signals brings: the attacker mindset of sharing compromise, except this time for good.&lt;/p&gt;

&lt;p&gt;Shared Signals open up theoretical branches of security not previously exposed in such a standard. Federated identities, external identities, cross-system identities can all be configured to share intel, to share signals in a framework that enables each platform to take the actions that fit their purpose. &lt;br&gt;
A memo is put out from a transmitter saying "this credential has been compromised." Some receivers may elect to issue communications to the impacted users. Others may quietly issue a credential reset. Some may decide to enforce MFA as a result. &lt;/p&gt;

&lt;p&gt;It is this democratised action that enables a view of &lt;strong&gt;shared security&lt;/strong&gt;. The source system issues standardised risk signals and any trusted downstream receiver takes the actions which fit that persona.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Spec references: &lt;a href="https://openid.net/specs/openid-sharedsignals-framework-1_0.html" rel="noopener noreferrer"&gt;OpenID SSF 1.0&lt;/a&gt;, &lt;a href="https://openid.net/specs/openid-caep-1_0.html" rel="noopener noreferrer"&gt;CAEP&lt;/a&gt;, &lt;a href="https://openid.net/specs/openid-risc-1_0.html" rel="noopener noreferrer"&gt;RISC&lt;/a&gt;, &lt;a href="https://datatracker.ietf.org/doc/html/rfc8417" rel="noopener noreferrer"&gt;RFC 8417 - Security Event Token&lt;/a&gt;, &lt;a href="https://datatracker.ietf.org/doc/html/rfc8935" rel="noopener noreferrer"&gt;RFC 8935 - Push Delivery&lt;/a&gt;, &lt;a href="https://datatracker.ietf.org/doc/html/rfc8936" rel="noopener noreferrer"&gt;RFC 8936 - Poll Delivery&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>identity</category>
      <category>zerotrust</category>
      <category>sharedsignals</category>
    </item>
    <item>
      <title>Why Your OAuth Flow Needs PKCE (And How It Actually Works)</title>
      <dc:creator>Mason Parle</dc:creator>
      <pubDate>Mon, 02 Feb 2026 22:13:40 +0000</pubDate>
      <link>https://dev.to/parlesec/why-your-oauth-flow-needs-pkce-and-how-it-actually-works-f96</link>
      <guid>https://dev.to/parlesec/why-your-oauth-flow-needs-pkce-and-how-it-actually-works-f96</guid>
      <description>&lt;p&gt;OAuth has moved well past the one-size-fits-all standard it was initially set to be. Human and non-human identity rely on it for all sorts of critical auth requirements, but as OAuth has expanded beyond the constraints of server-side apps, a gap has emerged: how do public clients prove their identity without a stored secret? &lt;/p&gt;

&lt;p&gt;That's where Proof Key for Code Exchange (PKCE, like 'pixie') comes in. PKCE is an extension to the &lt;strong&gt;authorization code flow&lt;/strong&gt; where users authenticate via browser redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Coat Check Problem
&lt;/h2&gt;

&lt;p&gt;Imagine you're at a conference and check your laptop bag. The standard procedure is you are given a claim ticket, you show the ticket and you get your bag.&lt;/p&gt;

&lt;p&gt;What if someone is looking over your shoulder and sees the ticket number? This opens the opportunity to abuse this 'shared secret' and recite "ticket 47" to retrieve your laptop.&lt;/p&gt;

&lt;p&gt;Now imagine a smarter system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you check your bag, you make up a secret phrase "purple elephant dancing"&lt;/li&gt;
&lt;li&gt;You whisper a &lt;em&gt;scrambled version&lt;/em&gt; of that phrase to the attendant (they can't reverse the scramble)&lt;/li&gt;
&lt;li&gt;They write down the scrambled version next to your bag&lt;/li&gt;
&lt;li&gt;Later, you come back and say the &lt;em&gt;original&lt;/em&gt; phrase&lt;/li&gt;
&lt;li&gt;They scramble it themselves and check if it matches what they wrote down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Someone who overheard the scrambled version can't readily reverse-engineer "purple elephant dancing."&lt;/p&gt;

&lt;p&gt;In a nutshell, that is PKCE. The secret phrase is your &lt;code&gt;code_verifier&lt;/code&gt;. The scrambled version is the &lt;code&gt;code_challenge&lt;/code&gt;. The attendant is the authorization server. The claim ticket is the authorization code. This is PKCE ensuring that even if someone intercepts your code, they can't use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do You Need PKCE?
&lt;/h2&gt;

&lt;p&gt;Short answer: yes, if you're using the authorization code flow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client type&lt;/th&gt;
&lt;th&gt;Classification&lt;/th&gt;
&lt;th&gt;PKCE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-page app (SPA)&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Required&lt;/strong&gt; - can't store secrets in browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile app&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Required&lt;/strong&gt; - URL schemes can be hijacked, binaries decompiled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop app&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Required&lt;/strong&gt; - binaries can be decompiled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI tool&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Required&lt;/strong&gt; - runs on user's machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side app&lt;/td&gt;
&lt;td&gt;Confidential&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Recommended&lt;/strong&gt; - defense in depth against code injection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It doesn't apply to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client credentials&lt;/strong&gt; - machine-to-machine, no authorization code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh token grants&lt;/strong&gt; - no authorization code involved&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implicit flow&lt;/strong&gt; - deprecated, tokens returned directly without a code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device authorization&lt;/strong&gt; - different mechanism, no redirect to intercept&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your flow has an authorization code, PKCE should protect it.&lt;br&gt;
The &lt;a href="https://datatracker.ietf.org/doc/rfc9700/" rel="noopener noreferrer"&gt;OAuth 2.0 Security Best Current Practice&lt;/a&gt; now recommends PKCE for &lt;em&gt;all&lt;/em&gt; OAuth clients, not just public ones. It adds defense in depth meaning even if you have a client secret, PKCE protects against authorization code injection attacks.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem PKCE Solves
&lt;/h2&gt;

&lt;p&gt;Traditional OAuth with a client secret was designed with server-side apps in mind, given the secret stays on your server, it isn't openly exposed during the authentication traffic.&lt;/p&gt;

&lt;p&gt;The challenge comes when considering SPAs and mobile applications, an emergent modern trend. There's no secure place to store a secret when the code runs on an external device and anyone who intercepts the authorization code can then exchange it for tokens.&lt;/p&gt;

&lt;p&gt;Proof Key for Code Exchange (PKCE) solves this by binding the token request to the original authorization request without requiring a stored secret.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Core Mechanism
&lt;/h2&gt;

&lt;p&gt;Going back to the coat check: instead of proving identity with a secret you store (the number 47 ticket), prove it by demonstrating you initiated the original request (purple elephant dancing).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-auth request&lt;/strong&gt;: Generate a random string (the &lt;code&gt;code_verifier&lt;/code&gt;) and keep it in memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-auth request&lt;/strong&gt;: Send a hash of that string (the &lt;code&gt;code_challenge&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-token request&lt;/strong&gt;: Send the original &lt;code&gt;code_verifier&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server verification&lt;/strong&gt;: Hash the verifier, compare to the stored challenge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An attacker who intercepts the authorization code doesn't have the original verifier and can't complete the token exchange.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Actual Implementation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Generate the Code Verifier
&lt;/h3&gt;

&lt;p&gt;The verifier is a cryptographically random string, 43-128 characters, using only &lt;code&gt;[A-Za-z0-9-._~]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateCodeVerifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;base64UrlEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;base64UrlEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/=/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Derive the Code Challenge
&lt;/h3&gt;

&lt;p&gt;The challenge is a SHA-256 hash of the verifier, base64url-encoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateCodeChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;base64UrlEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An interesting note. Given SHA-256 always outputs 32 bytes, base64url of 32 bytes (256/6 = 42.67).&lt;br&gt;
&lt;strong&gt;The challenge will always be 43 characters&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Authorization Request
&lt;/h3&gt;

&lt;p&gt;Include the challenge (&lt;strong&gt;not&lt;/strong&gt; the verifier) in your auth request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /authorize?
  response_type=code&amp;amp;
  client_id=your-app&amp;amp;
  redirect_uri=https://yourapp.com/callback&amp;amp;
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&amp;amp;
  code_challenge_method=S256&amp;amp;
  state=xyz123&amp;amp;
  scope=openid profile
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authorization server stores the challenge alongside your authorization session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Token Exchange
&lt;/h3&gt;

&lt;p&gt;After the user authenticates and you receive the code, exchange it with the original verifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authorizationCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourapp.com/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storedCodeVerifier&lt;/span&gt;  &lt;span class="c1"&gt;// The original, not the hash&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server hashes the verifier you send and compares it to the stored challenge. &lt;br&gt;
Match = tokens issued. No match = request rejected.&lt;/p&gt;
&lt;h2&gt;
  
  
  Common Misconfigurations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;plain&lt;/code&gt; instead of &lt;code&gt;S256&lt;/code&gt;&lt;/strong&gt;: The spec technically allows sending the verifier unhashed as the challenge (&lt;code&gt;code_challenge_method=plain&lt;/code&gt;). Don't. It exists only for clients that can't do SHA-256, which is essentially nothing modern. &lt;br&gt;
Always use S256. If you can't, there are bigger problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing the verifier insecurely&lt;/strong&gt;: The verifier should live in memory only. Don't put it in localStorage, sessionStorage, or cookies. &lt;br&gt;
For SPAs, keep it in a JavaScript variable. For mobile, use secure memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifier too short&lt;/strong&gt;: Must be 43-128 characters. Under 43 and the server should reject it. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong base64url encoding&lt;/strong&gt;: Standard base64 uses &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;. Base64url uses &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt;. Mixing them up breaks everything. Also strip the &lt;code&gt;=&lt;/code&gt; padding.&lt;/p&gt;
&lt;h2&gt;
  
  
  Seeing It In Action
&lt;/h2&gt;

&lt;p&gt;Now reading about PKCE is one thing, but watching the actual challenge/verifier exchange happen is thrilling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://protocolsoup.com/looking-glass" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Run the full PKCE flow in ProtocolSoup's Looking Glass&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://protocolsoup.com/" rel="noopener noreferrer"&gt;Protocol Soup&lt;/a&gt; was built specifically for this - bring tactility and interactivity to authentication protocols and identity in general. Run the real flow against a real authorization server and inspect every parameter at each step. &lt;br&gt;
The Looking Glass shows you the exact &lt;code&gt;code_challenge&lt;/code&gt; sent in the auth request, then the &lt;code&gt;code_verifier&lt;/code&gt; in the token request, so you can verify the cryptographic relationship yourself and compare against a traditional authorization code flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  When PKCE Isn't Enough
&lt;/h2&gt;

&lt;p&gt;Identity is an ever-growing frontier and PKCE was designed to protect the authorization code exchange. &lt;br&gt;
It doesn't protect against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token theft after issuance&lt;/strong&gt;: Once you have tokens, PKCE's job is done. Store and transmit them securely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compromised redirect URI&lt;/strong&gt;: If an attacker controls the redirect URI, they get the code &lt;em&gt;and&lt;/em&gt; can observe your token request. PKCE can't help here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS in your app&lt;/strong&gt;: If attackers can run JavaScript in your app, they can steal the verifier from memory before you use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PKCE can be boiled down to one principle: prove you started what you're trying to finish.&lt;/p&gt;




&lt;p&gt;For the authoritative source, see &lt;a href="https://datatracker.ietf.org/doc/html/rfc7636" rel="noopener noreferrer"&gt;RFC 7636&lt;/a&gt;.&lt;br&gt;
For current OAuth 2.0 Best Practice, see &lt;a href="https://datatracker.ietf.org/doc/rfc9700/" rel="noopener noreferrer"&gt;RFC 9700&lt;/a&gt;&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>security</category>
      <category>authentication</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
