Skip to content

Add JsonWebToken header-replacement constructor to avoid re-parsing payload#3530

Open
shankar-shubh wants to merge 4 commits into
AzureAD:devfrom
shankar-shubh:wilson/jwt-header-replacement-ctor
Open

Add JsonWebToken header-replacement constructor to avoid re-parsing payload#3530
shankar-shubh wants to merge 4 commits into
AzureAD:devfrom
shankar-shubh:wilson/jwt-header-replacement-ctor

Conversation

@shankar-shubh

Copy link
Copy Markdown

Add JsonWebToken header-replacement constructor to avoid re-parsing payload

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • If any gains or losses in performance are possible, you've included benchmarks for your changes. More info
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Summary of the changes (Less than 80 chars)

Add a JsonWebToken constructor that reuses a parsed payload on header replacement

Description

Adds a public constructor JsonWebToken(JsonWebToken jsonWebToken, string encodedHeader) that builds a token by replacing the header of an existing JWS while reusing its already-parsed payload and signature. The token is formed as encodedHeader.EncodedPayload.EncodedSignature; only the replacement header is decoded and parsed, so the payload claim set is reused instead of being parsed a second time.

This supports header-only transforms (for example PFT nonce hashing) where callers previously had to reassemble the token string and fully re-read it via ReadJsonWebToken, incurring a redundant payload parse and allocation.

Encrypted (JWE) source tokens are rejected with a new IDX14117 message because their plaintext payload is not available for reuse.

Changes

  • New public constructor on JsonWebToken (additive; existing parse paths unchanged).
  • New IDX14117 log message for the JWE-source rejection.
  • PublicAPI.Unshipped.txt updated for the new public API.
  • Unit tests: header replacement, payload/signature reuse, equivalence with a reparsed token, signature validation, null/empty guards, and the JWE rejection path.
  • JsonWebTokenHeaderReplacementBenchmarks isolating the new constructor against reassemble-and-reparse.

Benchmarks

Isolated microbenchmark (Microsoft.IdentityModel.Benchmarks, net8.0, MediumRun, MemoryDiagnoser) — reassemble-and-reparse vs. the new constructor:

Token size Re-parse Reuse-payload ctor Time Allocation
Minimal ~0.7 KB 2,549 ns / 2.63 KB 601 ns / 2.26 KB -76% (4.2x) -14%
Small ~1.9 KB 16,758 ns / 11.25 KB 818 ns / 4.81 KB -95% (20x) -57%
Typical ~4 KB 47,911 ns / 24.30 KB 1,079 ns / 8.85 KB -98% (44x) -64%

The eliminated payload parse scales with payload size, so the win grows with the token.

Fixes #3529

…ayload

Add a public constructor, JsonWebToken(JsonWebToken jsonWebToken, string
encodedHeader), that builds a token by replacing the header of an existing JWS
while reusing its already-parsed payload and signature. The token is formed as
'encodedHeader.EncodedPayload.EncodedSignature'; only the replacement header is
decoded and parsed, so the payload claim set is reused instead of being parsed a
second time.

This supports header-only transforms (for example PFT nonce hashing), where
callers previously had to reassemble the token string and fully re-read it,
incurring a redundant payload parse and allocation.

Encrypted (JWE) source tokens are rejected with a new IDX14117 message because
their plaintext payload is not available for reuse. Updates
PublicAPI.Unshipped.txt and adds unit tests covering header replacement,
payload/signature reuse, equivalence with a reparsed token, signature
validation, null/empty guards, and the JWE rejection path.

Adds JsonWebTokenHeaderReplacementBenchmarks isolating the new constructor
against reassemble-and-reparse across token sizes (net8.0, MediumRun,
MemoryDiagnoser):

  Minimal ~0.7 KB:  2549 ns / 2.63 KB   -> 601 ns / 2.26 KB   (-76% time, -14% alloc)
  Small   ~1.9 KB:  16758 ns / 11.25 KB -> 818 ns / 4.81 KB   (-95% time, -57% alloc)
  Typical ~4 KB:    47911 ns / 24.30 KB -> 1079 ns / 8.85 KB  (-98% time, -64% alloc)

The eliminated payload parse scales with payload size, so the win grows with the
token: at a realistic ~4 KB token the constructor is ~44x faster and allocates
64% less.
@shankar-shubh shankar-shubh requested a review from a team as a code owner June 22, 2026 09:28
@shankar-shubh

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

@iNinja

iNinja commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Nice perf win. One thought on the public surface: the new ctor reuses the original signature with a swapped header. That's exactly right for the PFT nonce-hashing case the description mentions (the new header reconstructs the originally-signed bytes), but as a general public ctor it lets any caller swap a header and silently keep an invalid signature.

Could we expose this through JsonWebTokenHandler instead, e.g.:

public virtual JsonWebToken ReplaceTokenHeader(JsonWebToken jsonWebToken, string encodedHeader)

Same payload-reuse perf, but it sits next to ReadJsonWebToken, reads as an intentional transform rather than a general-purpose ctor, and is mockable. The XML doc should note the signature is preserved as-is and only validates when the header reconstructs the originally-signed form (as in PFT). Suggest dropping the ctor from the public API and adding the handler method.

@shankar-shubh

Copy link
Copy Markdown
Author

@iNinja Have updated the PR as per the comment.

Comment thread src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs

@MZOLN MZOLN 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.

Left some comments

/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="jsonWebToken"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="encodedHeader"/> is null or empty.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="jsonWebToken"/> is an encrypted token (JWE), as its payload cannot be reused.</exception>

@bgavrilMS bgavrilMS Jul 13, 2026

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.

nit: it's not clear why JWEs aren't supported

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add JsonWebToken constructor to reuse a parsed payload when only the header changes

4 participants