Skip to content

fix(backend): preserve custom claims when verifying JWT M2M tokens#8697

Merged
jacekradko merged 2 commits into
mainfrom
jacek/m2m-jwt-custom-claims
May 29, 2026
Merged

fix(backend): preserve custom claims when verifying JWT M2M tokens#8697
jacekradko merged 2 commits into
mainfrom
jacek/m2m-jwt-custom-claims

Conversation

@jacekradko
Copy link
Copy Markdown
Member

JWT-format M2M tokens came back from client.m2m.verify() (and request-level auth()) with claims: null no matter what was baked into the token. M2MToken.fromJwtPayload hardcoded the claims constructor slot to null, so custom claims were dropped on the JWT path even though the opaque path returned them fine.

The JWT path is fully local (decode + signature check), so the payload is the only source of claims. The fix reconstructs them by excluding the reserved JWT/M2M claims that already map to their own fields. That denylist is the load-bearing part worth a look:

const M2M_RESERVED_JWT_CLAIMS = new Set(['iss', 'sub', 'aud', 'exp', 'nbf', 'iat', 'jti', 'scopes']);

A token with no custom claims still returns claims: null, matching the opaque path.

Fixes #8680.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment May 29, 2026 2:11am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: 95eb4a9

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

This PR includes changesets to release 10 packages
Name Type
@clerk/backend Patch
@clerk/astro Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch

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

@github-actions
Copy link
Copy Markdown
Contributor

Snapi: no API changes detected in @clerk/backend, @clerk/clerk-js, @clerk/nextjs, @clerk/react, @clerk/shared, @clerk/ui.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/dev-cli

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

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: 95eb4a9

@jacekradko jacekradko requested a review from wobsoriano May 28, 2026 20:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

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: 5d2604ea-a5c5-4dd7-a541-9a98ef07a782

📥 Commits

Reviewing files that changed from the base of the PR and between 2970146 and 95eb4a9.

📒 Files selected for processing (4)
  • .changeset/m2m-jwt-custom-claims.md
  • packages/backend/src/api/__tests__/M2MTokenApi.test.ts
  • packages/backend/src/api/resources/M2MToken.ts
  • packages/backend/src/api/resources/__tests__/M2MToken.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/backend/src/api/resources/tests/M2MToken.test.ts
  • packages/backend/src/api/resources/M2MToken.ts
  • packages/backend/src/api/tests/M2MTokenApi.test.ts

📝 Walkthrough

Walkthrough

This PR reconstructs and preserves user-defined claims for JWT-format M2M tokens by adding M2M_RESERVED_JWT_CLAIMS and an extractCustomClaims(payload) helper that removes only structural fields (iss, sub, exp, nbf, iat, jti). M2MToken.fromJwtPayload now sets token.claims from extractCustomClaims(payload) (or null if none). Tests were updated and added to verify reserved fields are stripped, custom claims (e.g., aud, permissions, role, scopes) are retained, scopes are parsed into token.scopes, and an integration test asserts m2mApi.verify preserves embedded custom claims. A package changeset documents the behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 name Status Explanation
Title check ✅ Passed The title directly and concisely describes the primary change: fixing custom claims preservation when verifying JWT M2M tokens.
Description check ✅ Passed The description clearly explains the bug, root cause, the fix approach with the denylist, and references the linked issue #8680.
Linked Issues check ✅ Passed The PR fully addresses issue #8680 by fixing M2MToken.fromJwtPayload to extract and preserve custom claims instead of forcing them to null.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the custom claims preservation issue; no unrelated modifications are present.

✏️ 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/backend/src/api/resources/M2MToken.ts (1)

20-34: 💤 Low value

Consider enhancing JSDoc with structured tags.

The function documentation is clear, but adding @param, @returns tags would improve consistency with project JSDoc standards for better IDE tooling support.

📝 Suggested JSDoc enhancement
 /**
  * Reconstructs the custom claims that were attached at token creation by
  * stripping the reserved JWT/M2M claims from the verified payload. Returns
  * `null` when no custom claims are present, matching the opaque-token path
  * where a token created without claims verifies back to `claims: null`.
+ *
+ * `@param` payload - The verified JWT payload containing both reserved and custom claims
+ * `@returns` The custom claims object, or null if no custom claims are present
  */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/src/api/resources/M2MToken.ts` around lines 20 - 34, Add
structured JSDoc tags to the existing comment for function extractCustomClaims:
include an `@param` tag describing the payload parameter (type M2MJwtPayload and
that it contains the verified JWT claims) and an `@returns` tag describing the
return type (Record<string, any> | null and when null is returned), and
optionally mention that reserved keys in M2M_RESERVED_JWT_CLAIMS are stripped;
keep the original summary but append these tags to match project JSDoc
standards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/backend/src/api/resources/M2MToken.ts`:
- Around line 20-34: Add structured JSDoc tags to the existing comment for
function extractCustomClaims: include an `@param` tag describing the payload
parameter (type M2MJwtPayload and that it contains the verified JWT claims) and
an `@returns` tag describing the return type (Record<string, any> | null and when
null is returned), and optionally mention that reserved keys in
M2M_RESERVED_JWT_CLAIMS are stripped; keep the original summary but append these
tags to match project JSDoc standards.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: 5a1315be-178f-45d0-8537-991c39cf0553

📥 Commits

Reviewing files that changed from the base of the PR and between 37535f9 and 2970146.

📒 Files selected for processing (4)
  • .changeset/m2m-jwt-custom-claims.md
  • packages/backend/src/api/__tests__/M2MTokenApi.test.ts
  • packages/backend/src/api/resources/M2MToken.ts
  • packages/backend/src/api/resources/__tests__/M2MToken.test.ts

Copy link
Copy Markdown
Member

@wobsoriano wobsoriano left a comment

Choose a reason for hiding this comment

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

Thanks for catching!

@jacekradko
Copy link
Copy Markdown
Member Author

@wobsoriano I pushed some additional hardening and logic changes.. can you re-review?

@jacekradko jacekradko requested a review from wobsoriano May 29, 2026 15:46
@wobsoriano
Copy link
Copy Markdown
Member

@wobsoriano I pushed some additional hardening and logic changes.. can you re-review?

thanks, I reviewed and it's good

@jacekradko
Copy link
Copy Markdown
Member Author

Thanks @wobsoriano ! 🏆

@jacekradko jacekradko merged commit be55c4e into main May 29, 2026
45 checks passed
@jacekradko jacekradko deleted the jacek/m2m-jwt-custom-claims branch May 29, 2026 16:41
Sign up for free to 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.

M2MToken.fromJwtPayload drops custom claims by forcing claims to null

2 participants