<?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>Forem: Blue Hills</title>
    <description>The latest articles on Forem by Blue Hills (@bluehills).</description>
    <link>https://forem.com/bluehills</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%2F3868296%2Ffae32375-3322-44cf-b7df-7fa58e895ccb.png</url>
      <title>Forem: Blue Hills</title>
      <link>https://forem.com/bluehills</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bluehills"/>
    <language>en</language>
    <item>
      <title>Auth regression tests for CI: what to assert and why</title>
      <dc:creator>Blue Hills</dc:creator>
      <pubDate>Sun, 03 May 2026 16:36:05 +0000</pubDate>
      <link>https://forem.com/bluehills/auth-regression-tests-for-ci-what-to-assert-and-why-1k92</link>
      <guid>https://forem.com/bluehills/auth-regression-tests-for-ci-what-to-assert-and-why-1k92</guid>
      <description>&lt;p&gt;Most teams I have worked with have one auth test in their suite. It looks like this:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valid token verifies&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="o"&gt;=&amp;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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signSync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api://backend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;secret&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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;That test is fine. It is also a smoke test, not a regression suite. It catches the case where verification is completely broken. It does not catch the case where verification accepts tokens that should be rejected — which is most of the auth bugs that ship to prod.&lt;/p&gt;

&lt;p&gt;A real auth regression suite asserts that &lt;strong&gt;invalid tokens fail with the right code&lt;/strong&gt;. Each test pairs a token with the failure mode it should produce. If the policy quietly accepts a token that should fail, the suite fails the PR. The audience configuration drift becomes visible the moment it's introduced, not three quarters later when someone writes the post-incident review.&lt;/p&gt;

&lt;p&gt;Here is the assertion catalog that has caught real bugs in real services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Wrong audience
&lt;/h2&gt;

&lt;p&gt;A token issued for service A should not authenticate against service B, even when both trust the same issuer. This is the most common configuration drift in microservice auth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token issued for api://reporting&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//billing&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;AUDIENCE_MISMATCH&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your billing service accepts a reporting-audience token, a reporting-audience credential becomes a billing credential. The fix is one config line. The test ensures the config line stays correct after the next refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Expired token
&lt;/h2&gt;

&lt;p&gt;A token whose &lt;code&gt;exp&lt;/code&gt; claim is in the past must be rejected. Allow a small clock skew (60 seconds is typical) but no more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token with exp = now() - 5 minutes&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;clock_skew_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;TOKEN_EXPIRED&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug this catches: a verifier that compares &lt;code&gt;exp&lt;/code&gt; against the wrong clock (server-side vs UTC vs local timezone), or that skips expiry checking entirely when &lt;code&gt;exp&lt;/code&gt; is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: &lt;code&gt;alg=none&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;An attacker sets &lt;code&gt;"alg":"none"&lt;/code&gt; in the JWT header and ships an unsigned token. If your verifier accepts the token's own &lt;code&gt;alg&lt;/code&gt; claim instead of using an explicit allowlist, the token will pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token with header.alg = none, no signature&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ALGORITHM_NOT_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This bug is eleven years old and still in production. Every JWT library has a way to fall into it. The regression test is the only thing that ensures your verifier is not in the family that does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: Algorithm confusion (RS256 → HS256)
&lt;/h2&gt;

&lt;p&gt;An attacker takes a token signed with RS256 (asymmetric) and re-signs it with HS256 (symmetric) using the public key as the shared secret. Some verifiers accept both algorithms with the same key material, and the public key happens to be valid as an HS256 secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token re-signed with HS256 using RS256 public key as secret&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ALGORITHM_NOT_ALLOWED&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test catches the bug class explicitly: even if your verifier code looks correct, the test ensures HS256 is rejected when only RS256 should be allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: Wrong issuer
&lt;/h2&gt;

&lt;p&gt;A token from &lt;code&gt;https://login.legitimate.com&lt;/code&gt; should not authenticate against &lt;code&gt;https://login.attacker.com&lt;/code&gt; or vice versa. The check has to be exact-match — no prefix substring, no glob.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token with iss = https://login.attacker.com&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.legitimate.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ISSUER_MISMATCH&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test catches verifiers that do &lt;code&gt;iss.startsWith(expected)&lt;/code&gt; or fuzzy hostname comparison. Both have shipped in production. Both let attacker-issuer tokens through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 6: Missing required claim
&lt;/h2&gt;

&lt;p&gt;If your service requires a &lt;code&gt;tenant_id&lt;/code&gt; claim, a token without it must be rejected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token without tenant_id claim&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;required_claims&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;sub&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tenant_id&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;REQUIRED_CLAIM_MISSING&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test catches drift where a new claim is added to your service's contract but the verifier config wasn't updated to require it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 7: Forged signature
&lt;/h2&gt;

&lt;p&gt;A token whose signature does not match the public key — anything from a copy-paste truncation to an active forgery — must be rejected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token with signature segment replaced with garbage&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;SIGNATURE_INVALID&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the smoke test in disguise. Every verifier should pass it. Run it anyway — the day it fails is the day someone replaced your verifier with a stub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 8: JWKS rotation drift
&lt;/h2&gt;

&lt;p&gt;The verifier should pick up new keys after the issuer rotates. If a token signed with a new key returns &lt;code&gt;KID_NOT_FOUND&lt;/code&gt; instead of &lt;code&gt;pass&lt;/code&gt;, the cache is stale and your fleet is about to break.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token signed with key issued AFTER last cache refresh&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;   &lt;span class="c1"&gt;# should pass — verifier should refetch JWKS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the test we run in CI nightly. If it ever fails, JWKS rotation just happened and our cache is now stale; the fix is to force a JWKS refetch in our verifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into CI
&lt;/h2&gt;

&lt;p&gt;The eight patterns above run as a single batch in jwtshield's &lt;code&gt;/v1/test/auth-regression&lt;/code&gt; endpoint. The endpoint accepts a list of &lt;code&gt;(token, policy, expected_failure_codes)&lt;/code&gt; tuples and returns a suite-level pass/fail plus per-check structured findings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redbullhorns/jwtshield-ci@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api://backend&lt;/span&gt;
    &lt;span class="na"&gt;fail-on-severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
    &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.JWTSHIELD_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Action runs in roughly 800ms. It costs nothing on the free tier (200 verifies per month). It uses synthetic test tokens — never your production tokens. The audit trail lives at &lt;code&gt;https://jwtshield.com/runs/&amp;lt;id&amp;gt;&lt;/code&gt; if you want compliance evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes this different from a smoke test
&lt;/h2&gt;

&lt;p&gt;A smoke test confirms verification works. A regression suite confirms verification rejects what it should — including the bug classes that have shipped to production for the last decade. The cost of writing the eight patterns is one afternoon. The cost of skipping them is one outage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jwtshield.com/signup" rel="noopener noreferrer"&gt;Get an API key →&lt;/a&gt; — or &lt;a href="https://github.com/marketplace/actions/jwtshield-ci" rel="noopener noreferrer"&gt;browse the GitHub Action listing&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Discuss on&lt;/strong&gt;: &lt;a href="https://news.ycombinator.com" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; · &lt;a href="https://dev.to/bluehills"&gt;dev.to&lt;/a&gt; · &lt;a href="https://hashnode.com/@bluehills" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; · &lt;a href="https://mastodon.social/@blue_hills" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/3-jwt-bugs-that-ship-to-prod-silently" rel="noopener noreferrer"&gt;Three JWT bugs that ship to prod silently&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/jwt-verification-8-check-field-guide" rel="noopener noreferrer"&gt;JWT verification in production: an 8-check field guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/alg-none-jwt-vulnerability" rel="noopener noreferrer"&gt;The alg=none JWT vulnerability, with code that exploits it and a 5-line fix&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>devsecops</category>
      <category>githubactions</category>
      <category>testing</category>
    </item>
    <item>
      <title>JWT verification in production: an 8-check field guide</title>
      <dc:creator>Blue Hills</dc:creator>
      <pubDate>Sun, 03 May 2026 16:31:40 +0000</pubDate>
      <link>https://forem.com/bluehills/jwt-verification-in-production-an-8-check-field-guide-am</link>
      <guid>https://forem.com/bluehills/jwt-verification-in-production-an-8-check-field-guide-am</guid>
      <description>&lt;p&gt;A correct JWT verifier does eight things. Most production verifiers I have read do four or five of them. The other three or four get skipped because the library defaults aren't loud about them, the docs gloss over them, or someone copied a "it works" snippet from Stack Overflow circa 2018.&lt;/p&gt;

&lt;p&gt;Here is the full eight-check list, what each one prevents, and what it looks like to implement them with structured error codes — the kind that survive a midnight 401 incident with a clear remediation path.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Signature
&lt;/h2&gt;

&lt;p&gt;Verify the cryptographic signature against a public key from the issuer's JWKS endpoint, scoped to the &lt;code&gt;kid&lt;/code&gt; (key id) in the JWT header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; signature is forged, key has been rotated and your cache is stale, or the JWKS endpoint is unreachable. The bug class is anything that lets a token through without signature verification — the &lt;code&gt;alg=none&lt;/code&gt; family, or libraries that accept the token's own &lt;code&gt;alg&lt;/code&gt; claim instead of an explicit allowlist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;SIGNATURE_INVALID&lt;/code&gt; or &lt;code&gt;KID_NOT_FOUND&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Issuer (&lt;code&gt;iss&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Match the token's &lt;code&gt;iss&lt;/code&gt; claim against your expected issuer URL, exactly. No prefix-substring tricks. No "starts with &lt;code&gt;https://login.&lt;/code&gt;" — that lets &lt;code&gt;https://login.attacker.com&lt;/code&gt; through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; the token came from a different issuer, the issuer rebranded its hostname (acquisition, region split), or your config has a typo. The third case is the silent failure mode — your tests pass against your test issuer, but production talks to a different one and you never noticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;ISSUER_MISMATCH&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Audience (&lt;code&gt;aud&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Match the token's &lt;code&gt;aud&lt;/code&gt; claim against your service's expected audience, exactly. The audience names which service the token is intended for. Skipping this check is the difference between "we have authentication" and "we have authorization" — without it, a token issued for &lt;code&gt;api://billing&lt;/code&gt; will authenticate against &lt;code&gt;api://reporting&lt;/code&gt; because both trust the same issuer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; the token was issued for a different service, or your config doesn't pin the audience at all. The "doesn't pin" case is the most common production bug. New endpoints get added; the audience check stays missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;AUDIENCE_MISMATCH&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Algorithm allowlist (&lt;code&gt;alg&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Specify allowed algorithms explicitly. Reject everything else. If you only ever issue HS256, your verifier must reject &lt;code&gt;HS512&lt;/code&gt;, &lt;code&gt;RS256&lt;/code&gt;, &lt;code&gt;PS512&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;, and every other algorithm — even though they are valid JWT algorithms.&lt;/p&gt;

&lt;p&gt;This is the check that prevents the &lt;code&gt;alg=none&lt;/code&gt; bug class and the RS256→HS256 confusion attack (where an attacker re-signs a token with the public key as if it were a shared secret). Both attacks are eleven years old and still in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; the token's &lt;code&gt;alg&lt;/code&gt; is not in your allowlist, or the allowlist is too permissive (e.g., includes both RS256 and HS256 with the same key material).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;ALGORITHM_NOT_ALLOWED&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Time (&lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Check that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;exp&lt;/code&gt; (expiration) is in the future,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nbf&lt;/code&gt; (not before) is in the past or absent,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iat&lt;/code&gt; (issued at) is plausible (not 5 years ago, not in the future).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Allow a small clock skew (typically 60 seconds) to handle the inevitable NTP drift across nodes. Larger skew is a smell — it usually means someone hit "valid token rejected because clock drift" once and over-corrected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; the token is expired, not yet valid, or backdated implausibly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;TOKEN_EXPIRED&lt;/code&gt;, &lt;code&gt;TOKEN_NOT_YET_VALID&lt;/code&gt;, &lt;code&gt;IAT_IMPLAUSIBLE&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Required claims
&lt;/h2&gt;

&lt;p&gt;Beyond the standard claims, your service may require certain custom claims to be present. A &lt;code&gt;sub&lt;/code&gt; (subject) is almost always required. A &lt;code&gt;tenant_id&lt;/code&gt; claim might be required for multi-tenant systems. A &lt;code&gt;scopes&lt;/code&gt; claim might gate access to specific endpoints.&lt;/p&gt;

&lt;p&gt;Required-claim checks are not built into JWT libraries — they are a contract between your verifier and your issuer. The contract has to be written down somewhere, and the verifier has to enforce it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; a required claim is missing or has the wrong type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;REQUIRED_CLAIM_MISSING&lt;/code&gt;, &lt;code&gt;CLAIM_TYPE_MISMATCH&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. JWKS reachability
&lt;/h2&gt;

&lt;p&gt;The verifier needs to fetch the JWKS endpoint to get public keys. If the endpoint is unreachable — DNS failure, certificate expired, 5xx response, network partition — you have a binary choice: fail closed (reject all tokens until JWKS is fetchable) or fail open (accept tokens with cached keys, eventually run out of cache).&lt;/p&gt;

&lt;p&gt;Most libraries fail open. That means a JWKS endpoint outage on the issuer side gives you minutes-to-hours of "tokens still verify, but we have no idea if they're still valid keys." This is fine for short outages. It is a security incident for long ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; JWKS endpoint returns 5xx, has TLS issues, or DNS-resolves to something unexpected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;JWKS_UNREACHABLE&lt;/code&gt;, &lt;code&gt;JWKS_TLS_ERROR&lt;/code&gt;, &lt;code&gt;JWKS_DNS_FAILURE&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. OIDC discovery integrity
&lt;/h2&gt;

&lt;p&gt;If you trust the OIDC discovery document (&lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;) to tell you the issuer, JWKS URI, and supported algorithms, you need to verify that the discovery document is consistent with what your verifier expects. The most common drift modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The issuer changes hostnames. Tokens carry the new hostname, your verifier expects the old one.&lt;/li&gt;
&lt;li&gt;The supported algorithms change. The issuer deprecates RS256 in favor of ES256.&lt;/li&gt;
&lt;li&gt;The JWKS URI moves. Your cached JWKS goes stale because the polling URL no longer returns keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pin the discovery document. Re-validate against it on every deploy, not on first call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails if:&lt;/strong&gt; discovery document fields don't match your verifier's pinned config, or have moved without your config catching up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure code:&lt;/strong&gt; &lt;code&gt;DISCOVERY_DRIFT&lt;/code&gt;, &lt;code&gt;JWKS_URI_MISMATCH&lt;/code&gt;, &lt;code&gt;ALG_POLICY_DRIFT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in production
&lt;/h2&gt;

&lt;p&gt;A correct verifier returns structured findings, not boolean true/false. For each check, emit:&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;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statuses"&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;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"audience"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"algorithm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required_claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&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;"findings"&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="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUDIENCE_MISMATCH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Token audience 'api://reporting' does not match expected 'api://billing'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"remediation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Update verifier config to accept 'api://reporting' OR reject this token"&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;The structured output is what makes JWT bugs debuggable. A boolean rejection tells you a token failed. A structured rejection tells you which check failed, why, and what to do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skip-the-implementation option
&lt;/h2&gt;

&lt;p&gt;If you do not want to wire all eight checks yourself, this is what jwtshield's &lt;code&gt;/v1/validate/jwt&lt;/code&gt; endpoint runs on every call. Pass a token and a policy; get a structured &lt;code&gt;VerifyResult&lt;/code&gt; back. The endpoint runs all eight checks every time, with consistent failure codes across all your services.&lt;/p&gt;

&lt;p&gt;In CI, drop the GitHub Actions wrapper into any workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redbullhorns/jwtshield-ci@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api://backend&lt;/span&gt;
    &lt;span class="na"&gt;allowed-algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RS256&lt;/span&gt;
    &lt;span class="na"&gt;fail-on-severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
    &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.JWTSHIELD_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five lines. Eight checks. Structured findings. Free tier covers 200 verifies per month.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jwtshield.com/signup" rel="noopener noreferrer"&gt;Get an API key →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Discuss on&lt;/strong&gt;: &lt;a href="https://news.ycombinator.com" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; · &lt;a href="https://dev.to/bluehills"&gt;dev.to&lt;/a&gt; · &lt;a href="https://hashnode.com/@bluehills" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; · &lt;a href="https://mastodon.social/@blue_hills" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/3-jwt-bugs-that-ship-to-prod-silently" rel="noopener noreferrer"&gt;Three JWT bugs that ship to prod silently&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/alg-none-jwt-vulnerability" rel="noopener noreferrer"&gt;The alg=none JWT vulnerability, with code that exploits it and a 5-line fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/auth0-jwt-validation-production" rel="noopener noreferrer"&gt;Validating Auth0 JWTs in production: the 8 checks Auth0's docs don't tell you&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/express-jwt-middleware-2026" rel="noopener noreferrer"&gt;Express JWT middleware in 2026: why express-jwt still has footguns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>jwt</category>
      <category>oidc</category>
      <category>security</category>
      <category>deved</category>
    </item>
    <item>
      <title>We rotated our JWKS without overlap. Here is the 4-minute window that broke prod.</title>
      <dc:creator>Blue Hills</dc:creator>
      <pubDate>Sun, 03 May 2026 16:31:34 +0000</pubDate>
      <link>https://forem.com/bluehills/we-rotated-our-jwks-without-overlap-here-is-the-4-minute-window-that-broke-prod-11i3</link>
      <guid>https://forem.com/bluehills/we-rotated-our-jwks-without-overlap-here-is-the-4-minute-window-that-broke-prod-11i3</guid>
      <description>&lt;p&gt;The on-call alert at 02:14 said &lt;code&gt;auth_5xx_rate spiked from 0.01 to 31.4&lt;/code&gt;. Not a deploy window. Not a traffic spike. Just thirty-one percent of authenticated requests failing for ~four minutes, then back to baseline.&lt;/p&gt;

&lt;p&gt;The cause was a JWKS rotation on the issuer side. New keys came in. Old keys went out. Caches in our service didn't refresh fast enough. Tokens signed with the new key were rejected because the verifier still held the old JWKS. Tokens signed with the old key were rejected because the issuer had stopped publishing them. We had a key-overlap gap of roughly four minutes between when the issuer stopped issuing tokens with the old key and when our verifier's cache picked up the new one.&lt;/p&gt;

&lt;p&gt;This is a class of bug that does not show up in any of the tests we run. Unit tests use a fixture JWKS that never rotates. Integration tests use a mocked issuer. Synthetic monitoring hits the live issuer but uses tokens minted within the same minute, so cache freshness is irrelevant. The bug only shows up in the seam between the issuer's rotation cadence and the verifier's cache TTL — a seam that exists only in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanic
&lt;/h2&gt;

&lt;p&gt;Identity providers rotate signing keys for two reasons: scheduled rotation (typically 24h to 30 days, depending on policy) and incident rotation (compromise suspected). The standard practice is &lt;strong&gt;overlap&lt;/strong&gt; — publish the new key in the JWKS endpoint several hours before issuing tokens with it, so every consumer's cache has time to refresh before the first token signed with the new key arrives.&lt;/p&gt;

&lt;p&gt;The overlap window has to be longer than the longest cache TTL across all consumers. Most JWKS-fetching libraries default to a 10-minute TTL. Some are 1 hour. Some hardcode a 24-hour cache and don't expose a refresh hook at all. If your overlap is shorter than your slowest consumer's TTL, you will see exactly what we saw: a brief window where new tokens are unverifiable because the consumer hasn't picked up the new key yet.&lt;/p&gt;

&lt;p&gt;Our issuer's overlap was 4 hours. The consumer with the slowest cache was a service we hadn't touched in six months, running an older version of &lt;code&gt;node-jose&lt;/code&gt; with a 24-hour TTL. The first token signed with the new key arrived 4 hours and 12 seconds after the rotation announcement. Cache TTL hadn't expired yet. 401s for the rest of the cache window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reproduction
&lt;/h2&gt;

&lt;p&gt;Spin up a JWKS server. Sign a token with key A. Verify it. Rotate the JWKS endpoint to key B with no overlap. Sign a new token with key B. Try to verify with the cached JWKS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERR: kid 'b1' not found in JWKS
ERR: signature verification failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the exact production error, just on a laptop. The painful part of the production version is that you cannot fix it from inside the verifier — by the time the alert pages, the rotation is already happening, and the only mitigation is to wait for the cache TTL to expire across the fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually did
&lt;/h2&gt;

&lt;p&gt;After the postmortem, three things changed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Fail loud, not silent.&lt;/strong&gt; We added explicit logging when the cache misses on a &lt;code&gt;kid&lt;/code&gt; lookup, and when JWKS refresh returns a different fingerprint than the cached one. The 4-minute window had been silent in our logs because the JWT library swallowed the cache miss and returned a generic "signature invalid" error. We could not tell from logs alone that this was a rotation problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Forced refresh on &lt;code&gt;kid&lt;/code&gt; miss.&lt;/strong&gt; Most JWT libraries don't refresh the JWKS when they hit a &lt;code&gt;kid&lt;/code&gt; they don't recognize — they fail closed and return an error. We patched our wrapper to force a JWKS refetch on the first &lt;code&gt;kid&lt;/code&gt; miss, then retry verification once. This shortens the window from "wait for cache TTL" to "one refetch round-trip" — sub-second for most clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. We added a CI check.&lt;/strong&gt; Every deploy now runs a JWKS rotation classifier against our issuer. If the issuer's current JWKS is &lt;code&gt;disjoint&lt;/code&gt; from the previous JWKS we recorded — meaning no key in the previous set is in the current set — the CI step fails the build with a structured finding. This is the check that would have caught the issuer-side overlap gap before anyone shipped tokens against it.&lt;/p&gt;

&lt;p&gt;The check we use is jwtshield's &lt;code&gt;/v1/validate/jwks-rotation&lt;/code&gt; endpoint. It accepts a previous JWKS, a current JWKS, an optional sample token, and an optional overlap policy. It returns one of &lt;code&gt;no_change&lt;/code&gt;, &lt;code&gt;safe_overlap&lt;/code&gt;, &lt;code&gt;overlap&lt;/code&gt;, or &lt;code&gt;disjoint&lt;/code&gt;. &lt;code&gt;disjoint&lt;/code&gt; means: every key has changed, no overlap window. That is the failure mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.jwtshield.com/v1/validate/jwks-rotation &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$JWTSHIELD_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "previous_jwks": &amp;lt;last-known good&amp;gt;,
    "current_jwks":  &amp;lt;freshly fetched&amp;gt;,
    "overlap_policy": { "min_overlap_count": 1 }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI, the &lt;code&gt;redbullhorns/jwtshield-ci@v1&lt;/code&gt; Action wraps this into a 5-line GHA step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redbullhorns/jwtshield-ci@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api://backend&lt;/span&gt;
    &lt;span class="na"&gt;fail-on-severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
    &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.JWTSHIELD_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The step fails the build if the rotation classifier returns &lt;code&gt;disjoint&lt;/code&gt; (no overlap), or if the overlap policy you set is violated. If the issuer is well-behaved and overlaps are 24h+, you never see this fire. The day someone forgets to set the overlap correctly, you find out before tokens ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;JWKS rotation is one of those bugs that is invisible until it ruins your night. Test fixtures don't replicate the seam. Mocked issuers don't either. The bug lives in the gap between the issuer's rotation policy and your verifier's cache TTL, and it stays invisible until the gap is shorter than the cache.&lt;/p&gt;

&lt;p&gt;The postmortem item that made the difference wasn't the cache fix or the logging change — those were good, but they shorten the blast radius once the bug fires. The CI check is what stops the bug from firing in the first place.&lt;/p&gt;

&lt;p&gt;If you've shipped a JWT validator in the last five years and you have not run &lt;code&gt;jwks-rotation&lt;/code&gt; against your issuer's last 30 days of JWKS publications, you have at least one of these gaps in your fleet right now.&lt;/p&gt;

&lt;p&gt;Read the full incident-class catalogue: &lt;a href="https://jwtshield.com/blog/3-jwt-bugs-that-ship-to-prod-silently" rel="noopener noreferrer"&gt;Three JWT bugs that ship to prod silently&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Free tier: 200 verifies per month, all algorithms, no card. &lt;a href="https://jwtshield.com/signup" rel="noopener noreferrer"&gt;Get an API key →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Discuss on&lt;/strong&gt;: &lt;a href="https://news.ycombinator.com" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; · &lt;a href="https://dev.to/bluehills"&gt;dev.to&lt;/a&gt; · &lt;a href="https://hashnode.com/@bluehills" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; · &lt;a href="https://mastodon.social/@blue_hills" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/3-jwt-bugs-that-ship-to-prod-silently" rel="noopener noreferrer"&gt;Three JWT bugs that ship to prod silently&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/alg-none-jwt-vulnerability" rel="noopener noreferrer"&gt;The alg=none JWT vulnerability, with code that exploits it and a 5-line fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/auth0-jwt-validation-production" rel="noopener noreferrer"&gt;Validating Auth0 JWTs in production: the 8 checks Auth0's docs don't tell you&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>jwt</category>
      <category>jwks</category>
      <category>devsecops</category>
      <category>sre</category>
    </item>
    <item>
      <title>Three JWT bugs that ship to prod silently — and the 5-line CI test that catches them</title>
      <dc:creator>Blue Hills</dc:creator>
      <pubDate>Sat, 02 May 2026 15:16:32 +0000</pubDate>
      <link>https://forem.com/bluehills/three-jwt-bugs-that-ship-to-prod-silently-and-the-5-line-ci-test-that-catches-them-5b5n</link>
      <guid>https://forem.com/bluehills/three-jwt-bugs-that-ship-to-prod-silently-and-the-5-line-ci-test-that-catches-them-5b5n</guid>
      <description>&lt;p&gt;Your auth tests pass. Your token verification works. Then your identity provider rotates a key at 02:47, your service hasn't refreshed its JWKS cache for 12 hours, and 8 minutes of production traffic hits 401.&lt;/p&gt;

&lt;p&gt;Or worse: the rotation does happen, your cache picks up the new keys, but a service you haven't touched in six months is still pinning the old &lt;code&gt;kid&lt;/code&gt;. Now half your fleet validates and half rejects, your error budget bleeds, and the only signal in your dashboard is "auth failures up."&lt;/p&gt;

&lt;p&gt;This is the silent-bug class. Your unit tests don't cover it because the tokens you generate in tests don't drift. Your integration tests don't cover it because mocked issuers are eternal. Snyk doesn't catch it because it's not a vulnerability in your code — it's a configuration that goes stale between your last deploy and the moment it matters.&lt;/p&gt;

&lt;p&gt;We built jwtshield to catch the three concrete failure modes that take down OIDC in production. Add a five-line GitHub Actions step. Each bug below is a real incident class with a reproduction and a one-line mitigation in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: JWKS rotation without overlap
&lt;/h2&gt;

&lt;p&gt;Your identity provider publishes signing keys at &lt;code&gt;https://login.example.com/.well-known/jwks.json&lt;/code&gt;. Your service caches that JWKS for some interval (10 minutes? An hour? Whatever your library defaults to). Tokens are signed by the current private key; verification uses the matching public key from the cache.&lt;/p&gt;

&lt;p&gt;The provider rotates keys. Best practice is to publish the &lt;strong&gt;new&lt;/strong&gt; key 24-48 hours before issuing tokens with it, so caches everywhere have time to pick it up. This is "overlap." Without it, the moment the provider switches signing keys, every cached JWKS in the world is stale until it refreshes.&lt;/p&gt;

&lt;p&gt;Most identity providers do overlap correctly. Some don't. Some teams misconfigure their own internal IdPs. The result is a ~3-minute window where new tokens reference a &lt;code&gt;kid&lt;/code&gt; that no verifier has seen yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproduction.&lt;/strong&gt; Spin up a JWKS server. Sign a token with key A. Verify it. Rotate the JWKS endpoint to key B with no overlap. Sign a new token with key B. Try to verify with the cached JWKS. You'll see one of two failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERR: kid 'b1' not found in JWKS
ERR: signature verification failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The check.&lt;/strong&gt; jwtshield's &lt;code&gt;/v1/validate/jwks-rotation&lt;/code&gt; accepts a previous JWKS, a current JWKS, an optional sample token, and an optional overlap policy. It returns one of &lt;code&gt;no_change | safe_overlap | overlap | disjoint&lt;/code&gt;. &lt;code&gt;disjoint&lt;/code&gt; means: no key from the previous set is in the current set. That's the failure mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.jwtshield.com/v1/validate/jwks-rotation &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$JWTSHIELD_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "previous_jwks": &amp;lt;last-known good&amp;gt;,
    "current_jwks":  &amp;lt;freshly fetched&amp;gt;,
    "overlap_policy": { "min_overlap_count": 1 }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run this on every deploy of the service that owns the issuer config, you catch the rotation gap before it ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: Wrong audience claim
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;aud&lt;/code&gt; claim in a JWT names the service the token is intended for. A token issued for &lt;code&gt;api://billing&lt;/code&gt; should not authenticate against &lt;code&gt;api://reporting&lt;/code&gt;. This is the audience check, and it is the difference between "we have auth" and "we have authorization."&lt;/p&gt;

&lt;p&gt;The bug: a service accepts any well-signed token from a trusted issuer, regardless of &lt;code&gt;aud&lt;/code&gt;. A user signs in to billing, billing issues a token, the user replays the token against reporting, and reporting hands back the user's data. The signature is valid. The expiry is fresh. The issuer is on the allowlist. The only thing wrong is that this token was never meant for this service.&lt;/p&gt;

&lt;p&gt;This is a configuration bug. The verifier on reporting was set up six quarters ago by an engineer who has since left, and it doesn't pin the audience. New endpoints get added; the audience check stays missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproduction.&lt;/strong&gt; Use any JWT library that accepts an "issuer" but not an "audience" parameter. Issue a token from your IdP for service A. Send it to service B. Most setups let it through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check.&lt;/strong&gt; jwtshield's &lt;code&gt;/v1/test/auth-regression&lt;/code&gt; accepts a list of &lt;code&gt;(token, expected_failure_codes)&lt;/code&gt; tuples and runs them against your policy. Add one entry per service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token issued for api://reporting&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;//billing&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;allowed_algs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RS256&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;expected_failure_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;AUDIENCE_MISMATCH&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite passes only if the token correctly fails with &lt;code&gt;AUDIENCE_MISMATCH&lt;/code&gt;. If the policy quietly accepts it, the suite fails the PR. The audience configuration drift becomes visible the moment it's introduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: Issuer config drift (the OIDC discovery doc lies)
&lt;/h2&gt;

&lt;p&gt;Every OIDC provider exposes a discovery document at &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;. It lists the issuer URL, JWKS URI, supported algorithms, and the endpoints clients need. Your service reads it once at startup, caches the values, and verifies tokens against the cached config.&lt;/p&gt;

&lt;p&gt;The provider updates the discovery doc. The cached config is now stale. The most common drift modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The issuer changes hostnames (acquisition, rebrand, region split). Tokens carry &lt;code&gt;iss: https://new.example.com&lt;/code&gt;, your verifier expects &lt;code&gt;https://old.example.com&lt;/code&gt;, validation fails.&lt;/li&gt;
&lt;li&gt;The supported algorithms change. The provider deprecates RS256 in favor of ES256. Your verifier accepts both, so tokens still validate, but the policy you intended to enforce is now wrong.&lt;/li&gt;
&lt;li&gt;The JWKS URI moves. Your cached JWKS goes stale because the polling URL no longer returns keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reproduction.&lt;/strong&gt; Set up a verifier that caches the discovery doc on first call. Update the discovery doc on the provider side. Wait for the next token request. Validation passes against stale config until something visible breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check.&lt;/strong&gt; jwtshield's &lt;code&gt;/v1/lint/oidc-config&lt;/code&gt; takes the issuer URL, expected audiences, allowed algorithms, JWKS URI, and redirect URIs. It fetches the live discovery doc, fetches the live JWKS, and emits structured findings:&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;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"findings"&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="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWKS_URI_MISMATCH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Configured JWKS URI does not match discovery document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&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;"configured"&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://login.example.com/.well-known/jwks.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"discovered"&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://login.example.com/oauth/jwks"&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;"remediation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Update your verifier configuration to use the discovered URI..."&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;Run it nightly against your prod issuer. The first time the discovery doc moves, you find out before your customers do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: five lines of CI
&lt;/h2&gt;

&lt;p&gt;All three checks ship in jwtshield-ci, our GitHub Actions wrapper. Add this to any workflow that touches your auth path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redbullhorns/jwtshield-ci@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://login.example.com&lt;/span&gt;
    &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api://backend&lt;/span&gt;
    &lt;span class="na"&gt;fail-on-severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Action calls jwtshield's regression suite with your policy, prints a structured status table, and fails the build on any high-severity finding. It runs in roughly 800ms. It costs nothing on the free tier (200 verifies/month).&lt;/p&gt;

&lt;p&gt;We send synthetic test tokens, never your production tokens. Tokens are validated in memory and discarded — zero retention. Your audit trail lives at &lt;code&gt;https://jwtshield.com/runs/&amp;lt;id&amp;gt;&lt;/code&gt; if you want compliance evidence.&lt;/p&gt;

&lt;p&gt;The full status table on a passing run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;◼ jwtshield-ci v1.0.0 ─────────────────────────────────
  signature:        ✓ PASS
  issuer:           ✓ PASS
  audience:         ✓ PASS
  algorithm:        ✓ PASS
  time:             ✓ PASS
  required_claims:  ✓ PASS
  ─────────────────────────────────────────────────────
  6/6 checks passed · 0 findings
  evidence: https://jwtshield.com/runs/abc123def456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Free tier: 200 verifies per month, all algorithms, community support. No credit card.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Get a key&lt;/span&gt;
open https://jwtshield.com/signup

&lt;span class="c"&gt;# 2. Run the rotation classifier locally&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.jwtshield.com/v1/validate/jwks-rotation &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$JWTSHIELD_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @rotation.json

&lt;span class="c"&gt;# 3. Add the Action to your CI&lt;/span&gt;
&lt;span class="c"&gt;# .github/workflows/auth.yml&lt;/span&gt;
- uses: redbullhorns/jwtshield-ci@v1
  with:
    issuer: https://login.example.com
    audience: api://backend
    fail-on-severity: high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pricing: $0 Starter (200 verifies, 1 issuer) → $49 Developer → $99 Startup → $199 Team → custom Enterprise. The Team tier covers 50,000 verifies a month, 25 issuers, full CI regression suite, and 30-day evidence retention.&lt;/p&gt;

&lt;p&gt;If you've shipped a JWT validator in the last five years, you have at least one of these three bugs latent in production. The check is five lines.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Discuss on&lt;/strong&gt;: &lt;a href="https://news.ycombinator.com" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; · &lt;a href="https://dev.to/bluehills"&gt;dev.to&lt;/a&gt; · &lt;a href="https://hashnode.com/@bluehills" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; · &lt;a href="https://mastodon.social/@blue_hills" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/alg-none-jwt-vulnerability" rel="noopener noreferrer"&gt;The alg=none JWT vulnerability, with code that exploits it and a 5-line fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/auth0-jwt-validation-production" rel="noopener noreferrer"&gt;Validating Auth0 JWTs in production: the 8 checks Auth0's docs don't tell you&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jwtshield.com/blog/express-jwt-middleware-2026" rel="noopener noreferrer"&gt;Express JWT middleware in 2026: why express-jwt still has footguns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>jwt</category>
      <category>oidc</category>
      <category>devsecops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Stop Sending Raw Clinical Notes to Your AI Stack</title>
      <dc:creator>Blue Hills</dc:creator>
      <pubDate>Wed, 08 Apr 2026 17:45:56 +0000</pubDate>
      <link>https://forem.com/bluehills/stop-sending-raw-clinical-notes-to-your-ai-stack-376e</link>
      <guid>https://forem.com/bluehills/stop-sending-raw-clinical-notes-to-your-ai-stack-376e</guid>
      <description>&lt;h2&gt;
  
  
  Clinical Note De-identifier API: De-Identify Clinical Notes Before AI Processing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A privacy-first API for healthcare developers building LLM, analytics, and search workflows
&lt;/h3&gt;

&lt;p&gt;If you’re building healthtech software with LLMs, clinical text processing, medical note summarization, analytics, or search pipelines, you’ve probably run into the same problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;clinical notes are incredibly useful — and incredibly sensitive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They contain names, dates, phone numbers, addresses, MRNs, IDs, and other patient-identifiable information that should not casually flow through every downstream service in your stack.&lt;/p&gt;

&lt;p&gt;That creates friction for developers.&lt;/p&gt;

&lt;p&gt;You want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;summarize notes with AI&lt;/li&gt;
&lt;li&gt;classify records&lt;/li&gt;
&lt;li&gt;extract insights&lt;/li&gt;
&lt;li&gt;build internal tooling faster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But before any of that, you need a clean way to &lt;strong&gt;de-identify the text&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is exactly why I built &lt;strong&gt;Clinical Note De-identifier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is now publicly listed on RapidAPI as &lt;strong&gt;Clinical Note De-identifier&lt;/strong&gt;, which makes it easier for developers to discover the API, review the listing, and integrate it into their own workflows through the &lt;a href="https://rapidapi.com/blue-hills-blue-hills-default/api/clinical-note-de-identifier" rel="noopener noreferrer"&gt;RapidAPI marketplace listing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Clinical Note De-identifier API does
&lt;/h2&gt;

&lt;p&gt;Clinical Note De-identifier is an API that helps remove or mask sensitive information from clinical note text before it moves into downstream systems.&lt;/p&gt;

&lt;p&gt;Think of it as a preprocessing layer for healthcare-adjacent developer workflows.&lt;/p&gt;

&lt;p&gt;You send in raw note text like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Patient: John Doe
DOB: 04/14/1982
MRN: 842991
Seen at North Valley Clinic on 03/21/2026.
Phone: 555-123-8841

Assessment:
Patient reports worsening lower back pain for the last 3 weeks...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And get back de-identified output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Patient: [REDACTED_NAME]
DOB: [REDACTED_DATE]
MRN: [REDACTED_ID]
Seen at [REDACTED_LOCATION] on [REDACTED_DATE].
Phone: [REDACTED_PHONE]

Assessment:
Patient reports worsening lower back pain for the last 3 weeks...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;preserve the clinical value of the note while reducing exposure of sensitive identifiers.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for healthcare developers
&lt;/h2&gt;

&lt;p&gt;A lot of API products in healthcare get framed around compliance teams, enterprise workflows, or procurement-heavy platforms.&lt;/p&gt;

&lt;p&gt;But there’s also a very practical developer problem here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How do I safely use clinical text in my app without passing raw identifiers everywhere?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That problem shows up in real products like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI note summarizers&lt;/li&gt;
&lt;li&gt;chart review assistants&lt;/li&gt;
&lt;li&gt;search/indexing systems&lt;/li&gt;
&lt;li&gt;analytics dashboards&lt;/li&gt;
&lt;li&gt;coding assistance tools&lt;/li&gt;
&lt;li&gt;triage automation&lt;/li&gt;
&lt;li&gt;data labeling pipelines&lt;/li&gt;
&lt;li&gt;internal QA or demo environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are prototyping or shipping tools in this space, de-identification is not a “nice to have” step. It is foundational.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a clinical note de-identification API matters
&lt;/h2&gt;

&lt;p&gt;Healthcare AI developers need a practical way to remove protected health information from raw note text before it reaches downstream systems. A &lt;strong&gt;clinical note de-identification API&lt;/strong&gt; helps teams reduce unnecessary exposure of names, dates, identifiers, phone numbers, and locations while preserving the medical context needed for summarization, classification, search, and analytics.&lt;/p&gt;

&lt;p&gt;That makes this kind of API useful for teams searching for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clinical note de-identification API&lt;/li&gt;
&lt;li&gt;PHI redaction API&lt;/li&gt;
&lt;li&gt;healthcare text anonymization API&lt;/li&gt;
&lt;li&gt;de-identification before LLM processing&lt;/li&gt;
&lt;li&gt;clinical note privacy tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common use cases
&lt;/h2&gt;

&lt;p&gt;Here are a few places an API like this fits naturally:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Before sending notes to an LLM
&lt;/h3&gt;

&lt;p&gt;If you are using AI to summarize, classify, or transform note content, de-identifying first adds a cleaner privacy boundary in your pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Before indexing notes for search
&lt;/h3&gt;

&lt;p&gt;Search systems do not need a patient’s name or phone number to understand medical context.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. For analytics and reporting
&lt;/h3&gt;

&lt;p&gt;Teams often want trends and patterns, not direct identifiers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. For staging, testing, and demos
&lt;/h3&gt;

&lt;p&gt;Demo data often starts as “temporarily sanitized later.” That is risky. A de-identifier makes this step repeatable.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. For partner-facing integrations
&lt;/h3&gt;

&lt;p&gt;When data is moving between systems, every unnecessary identifier increases exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use an API instead of writing regex everywhere?
&lt;/h2&gt;

&lt;p&gt;Because regex-only approaches usually start simple and turn messy fast.&lt;/p&gt;

&lt;p&gt;Clinical notes are unstructured. Real-world text is inconsistent. Formats vary across systems, writers, and facilities.&lt;/p&gt;

&lt;p&gt;Hand-rolled redaction logic often becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;brittle&lt;/li&gt;
&lt;li&gt;hard to maintain&lt;/li&gt;
&lt;li&gt;difficult to audit&lt;/li&gt;
&lt;li&gt;inconsistent across note types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An API gives you a cleaner interface for plugging de-identification into your workflow without rebuilding the same logic in every service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer-first API design
&lt;/h2&gt;

&lt;p&gt;I wanted this to feel useful for developers, not just procurement decks.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;straightforward API usage&lt;/li&gt;
&lt;li&gt;easy integration into preprocessing pipelines&lt;/li&gt;
&lt;li&gt;usable for prototypes and production-minded systems&lt;/li&gt;
&lt;li&gt;focused on practical text redaction workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ideal flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clinical Note --&amp;gt; De-identifier API --&amp;gt; Safe downstream processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clinical Note --&amp;gt; hope everyone handles PHI carefully --&amp;gt; problems later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where to find the Clinical Note De-identifier API
&lt;/h2&gt;

&lt;p&gt;You can access the public RapidAPI listing here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RapidAPI listing:&lt;/strong&gt; &lt;a href="https://rapidapi.com/blue-hills-blue-hills-default/api/clinical-note-de-identifier" rel="noopener noreferrer"&gt;Clinical Note De-identifier on RapidAPI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That gives developers a simple entry point to explore the API as a marketplace product instead of treating it like a private internal service. For an API like this, that matters: discoverability, onboarding, and fast evaluation are part of the product experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example workflow
&lt;/h2&gt;

&lt;p&gt;A basic architecture might look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive raw note text&lt;/li&gt;
&lt;li&gt;Send it to the de-identifier&lt;/li&gt;
&lt;li&gt;Store or forward only the redacted version&lt;/li&gt;
&lt;li&gt;Use that output for:

&lt;ul&gt;
&lt;li&gt;summarization&lt;/li&gt;
&lt;li&gt;classification&lt;/li&gt;
&lt;li&gt;search&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;review workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pseudo-example:&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="s2"&gt;YOUR_API_ENDPOINT&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="s2"&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="s2"&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="s2"&gt;application/json&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="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rawClinicalNote&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;result&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redacted_text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That simple preprocessing step can make the rest of your pipeline much safer and cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on trust
&lt;/h2&gt;

&lt;p&gt;Healthcare data requires care.&lt;/p&gt;

&lt;p&gt;This API is meant to help reduce exposure of sensitive information in developer workflows, but it should be used as part of a broader privacy and security approach, not as a magic checkbox.&lt;/p&gt;

&lt;p&gt;Good engineering here means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minimizing where raw notes travel&lt;/li&gt;
&lt;li&gt;redacting early&lt;/li&gt;
&lt;li&gt;logging carefully&lt;/li&gt;
&lt;li&gt;validating outputs&lt;/li&gt;
&lt;li&gt;applying appropriate legal, security, and compliance review for your use case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: &lt;strong&gt;de-identification should be a core layer in the pipeline, not an afterthought.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I like APIs that solve a concrete bottleneck.&lt;/p&gt;

&lt;p&gt;Clinical text is valuable. But the moment raw identifiers are mixed into everything, teams slow down, risk goes up, and every downstream integration becomes harder.&lt;/p&gt;

&lt;p&gt;I built Clinical Note De-identifier to make that first step easier:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;take raw clinical notes in, produce cleaner text out, and make the rest of the workflow more usable.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts on privacy-first clinical note processing
&lt;/h2&gt;

&lt;p&gt;If you’re building in healthtech, there’s a good chance your real product is not “redaction.”&lt;/p&gt;

&lt;p&gt;Your product might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an AI assistant&lt;/li&gt;
&lt;li&gt;a search tool&lt;/li&gt;
&lt;li&gt;an internal dashboard&lt;/li&gt;
&lt;li&gt;an automation workflow&lt;/li&gt;
&lt;li&gt;an analytics platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But de-identification is often the layer that makes those products safer to build.&lt;/p&gt;

&lt;p&gt;That is where this API fits.&lt;/p&gt;

&lt;p&gt;If you’re working on privacy-aware healthcare workflows and want a simpler way to preprocess note text, check out &lt;a href="https://rapidapi.com/blue-hills-blue-hills-default/api/clinical-note-de-identifier" rel="noopener noreferrer"&gt;Clinical Note De-identifier on RapidAPI&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
