') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(backend): Fix JWT array audience validation by jescalan · Pull Request #8470 · clerk/javascript · GitHub
Skip to content

fix(backend): Fix JWT array audience validation - #8470

Merged
wobsoriano merged 6 commits into
mainfrom
codex/fix-verify-token-array-aud
May 14, 2026
Merged

fix(backend): Fix JWT array audience validation#8470
wobsoriano merged 6 commits into
mainfrom
codex/fix-verify-token-array-aud

Conversation

@jescalan

@jescalanjescalan commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

This fixes JWT audience validation for tokens whose aud claim is an array.

verifyJwt now passes the raw aud claim and configured audience option into assertAudienceClaim. The assertion helper already normalizes string and string-array inputs, so this lets it evaluate array audiences correctly.

Scope

This does not mean default Clerk session tokens have an aud claim. They generally do not, and this PR does not change the existing behavior for tokens with no aud: if the token has no audience claim, verifyJwt still skips audience validation.

The bug only applies when both of these are true:

  • the caller configured audience
  • the token payload contains aud as a string array

That shape is valid JWT syntax and is already represented in this package's M2M token parsing fallback (aud?: string[]). Clerk-issued M2M tokens also allow custom claims, and aud is not reserved there. So the fix is scoped to correctly enforcing the existing audience option when an array aud is present, not to introducing audience binding for all Clerk tokens.

Root Cause

verifyJwt wrapped both values before calling the assertion:

assertAudienceClaim([aud],[audience]);

When the token already had aud as a string array, the assertion received a nested array. That shape skipped both validation branches: it was not a string, and it was not an array of strings. The function returned without enforcing the configured audience.

Tests

  • Added assertion coverage for two string arrays with no intersection.
  • Added verifier coverage using signed JWTs:
    • accepts an array aud when it includes the configured audience
    • rejects an array aud when it does not include the configured audience

Validation

  • NODE_OPTIONS=--no-experimental-webstorage pnpm --filter @clerk/backend build
  • pnpm --filter @clerk/backend build:runtime
  • NODE_OPTIONS=--no-experimental-webstorage pnpm exec vitest run src/jwt/__tests__/assertions.test.ts src/jwt/__tests__/verifyJwt.test.ts --environment node --typecheck.enabled=false
  • pnpm --filter @clerk/backend format:check
  • git diff --check

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentMay 14, 2026 4:24pm

Request Review

@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cc75a63

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
NameType
@clerk/backendPatch
@clerk/astroPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/honoPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jescalanjescalan changed the title [codex] Fix JWT array audience validationfix(backend): Fix JWT array audience validationMay 4, 2026
@jescalan
jescalan requested review from brkalow, dominic-clerk and jacekradko and removed request for brkalowMay 4, 2026 19:18
@jescalan
jescalan marked this pull request as ready for review May 4, 2026 19:18
@coderabbitai

coderabbitaiBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: fc3120d1-25d0-45c3-b736-8f1f47559a41

📥 Commits

Reviewing files that changed from the base of the PR and between 5c2ce3b and b1d1a8e.

📒 Files selected for processing (1)
  • packages/backend/src/jwt/__tests__/verifyJwt.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/backend/src/jwt/tests/verifyJwt.test.ts

📝 Walkthrough

Walkthrough

verifyJwt now calls assertAudienceClaim(aud, audience) without wrapping values in arrays. Tests added: a unit test for assertAudienceClaim that expects an "Invalid JWT audience claim array" error when arrays do not intersect, and two verifyJwt tests that sign tokens with aud as an array — one verifies success when the configured audience is present, the other verifies failure when it is absent.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix(backend): Fix JWT array audience validation' accurately and concisely describes the main change: fixing JWT audience validation when the aud claim is an array.
Description check✅ PassedThe description comprehensively explains the fix, root cause, scope, tests added, and validation steps performed, all directly related to the JWT array audience validation changes.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@dominic-clerkdominic-clerk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be for a follow-up but I think we could change the types in

exportconstassertAudienceClaim=(aud?: unknown,audience?: unknown)=>{
from unknown to string | string[]

Also I understand skipping verification if we have no expected audience, but it seems odd to skip it if we do expect something and the token doesn't provide any.

constshouldVerifyAudience=audienceList.length>0&&audList.length>0;

@pkg-pr-new

pkg-pr-newBot commented May 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8470

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8470

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8470

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8470

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8470

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8470

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8470

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8470

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8470

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8470

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8470

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8470

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8470

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8470

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8470

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8470

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8470

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8470

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8470

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8470

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8470

commit: cc75a63

@wobsorianowobsoriano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dominic-clerk approving as this fixes array aud handling and adds targeted coverage.

The type cleanup and missing aud behavior question can be a follow-up, right?

@dominic-clerk

Copy link
Copy Markdown
Contributor

@wobsoriano yes it can be a follow-up, what would be the best way to ensure it doesn't fall through cracks? A ticket in SDK team?

@wobsoriano

wobsoriano commented May 14, 2026

Copy link
Copy Markdown
Member

@wobsoriano yes it can be a follow-up, what would be the best way to ensure it doesn't fall through cracks? A ticket in SDK team?

Yes, SDK team.

Created SDK-89.

@wobsoriano
wobsoriano merged commit ee25cf2 into mainMay 14, 2026
43 checks passed
@wobsoriano
wobsoriano deleted the codex/fix-verify-token-array-aud branch May 14, 2026 16:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jescalan@dominic-clerk@wobsoriano