Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,59 @@ public IDictionary<string, string> InboundClaimTypeMap
}
}

/// <summary>
/// Determines if the <see cref="ReadOnlyMemory{T}"/> is a well formed JSON Web Token (JWT). See: <see href="https://datatracker.ietf.org/doc/html/rfc7519"/>.
/// </summary>
/// <param name="token"><see cref="ReadOnlyMemory{T}"/> that should represent a valid JWT.</param>
/// <remarks>Uses <see cref="Regex.IsMatch(string, string)"/> matching:
/// <para>JWS: @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$"</para>
/// <para>JWE: (dir): @"^[A-Za-z0-9-_]+\.\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$"</para>
/// <para>JWE: (wrappedkey): @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]$"</para>
/// </remarks>
/// <returns>
/// <para><see langword="false"/> if the token is null or whitespace.</para>
/// <para><see langword="false"/> if token.Length is greater than <see cref="TokenHandler.MaximumTokenSizeInBytes"/>.</para>
/// <para><see langword="true"/> if the token is in JSON Compact Serialization format.</para>
/// </returns>
public virtual bool CanReadToken(in ReadOnlyMemory<char> token)
{
if (token.IsEmpty || token.Span.IsWhiteSpace())
return false;

if (token.Length > MaximumTokenSizeInBytes)
{
if (LogHelper.IsEnabled(EventLogLevel.Informational))
LogHelper.LogInformation(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes));

return false;
}

// Count the number of segments, which is the number of periods + 1. We can stop when we've encountered
// more segments than the maximum we know how to handle.
int segmentCount = JwtTokenUtilities.CountJwtTokenPart(token.Span, JwtConstants.MaxJwtSegmentCount + 1);

switch (segmentCount)
Copy link
Contributor

@brentschmaltz brentschmaltz May 12, 2025

Choose a reason for hiding this comment

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

this code (switch (segmentCount)) can be shared between JsonWebTokenHandler and JwtSecurityTokenHandler.

When you think about it, most of this code could be shared between JsonWebTokenHandler and JwtSecurityTokenHandler.

Copy link
Author

Choose a reason for hiding this comment

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

@brentschmaltz please resolve if you're satisfied with the solution :)

{
case JwtConstants.JwsSegmentCount:
#if NET8_0_OR_GREATER
return JwtTokenUtilities.RegexJws.IsMatch(token.Span);
#else
return JwtTokenUtilities.RegexJws.IsMatch(token.ToString());
#endif

case JwtConstants.JweSegmentCount:
#if NET8_0_OR_GREATER
return JwtTokenUtilities.RegexJwe.IsMatch(token.Span);
#else
return JwtTokenUtilities.RegexJwe.IsMatch(token.ToString());
#endif

default:
LogHelper.LogInformation(LogMessages.IDX14107);
return false;
}
}

/// <summary>
/// Determines if the string is a well formed JSON Web Token (JWT). See: <see href="https://datatracker.ietf.org/doc/html/rfc7519"/>.
/// </summary>
Expand All @@ -137,6 +190,9 @@ public virtual bool CanReadToken(string token)
if (string.IsNullOrWhiteSpace(token))
return false;

#if NET8_0_OR_GREATER
return CanReadToken(token.AsMemory());
#else
if (token.Length > MaximumTokenSizeInBytes)
{
if (LogHelper.IsEnabled(EventLogLevel.Informational))
Expand Down Expand Up @@ -167,6 +223,7 @@ public virtual bool CanReadToken(string token)
LogHelper.LogInformation(LogMessages.IDX14107);
return false;
}
#endif
}

private static StringComparison GetStringComparisonRuleIf509(SecurityKey securityKey) =>
Expand Down
15 changes: 10 additions & 5 deletions src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -585,24 +585,29 @@ internal static SecurityKey ResolveTokenSigningKey(string kid, string x5t, IEnum
/// <param name="token">The JWT token.</param>
/// <param name="maxCount">The maximum number of segments to count up to.</param>
/// <returns>The number of segments up to <paramref name="maxCount"/>.</returns>
internal static int CountJwtTokenPart(string token, int maxCount)
internal static int CountJwtTokenPart(in ReadOnlySpan<char> token, int maxCount)
{
var count = 1;
var index = 0;
while (index < token.Length)
int count = 1;
int index = 0;
ReadOnlySpan<char> localToken = token;
while (index < localToken.Length)
{
var dotIndex = token.IndexOf('.', index);
int dotIndex = localToken.IndexOf('.');
if (dotIndex < 0)
{
break;
}

count++;
index = dotIndex + 1;
if (count == maxCount)
{
break;
}

localToken = localToken[index..];
}

return count;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
virtual Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CanReadToken(in System.ReadOnlyMemory<char> token) -> bool
61 changes: 59 additions & 2 deletions src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,59 @@ public override Type TokenType
get { return typeof(JwtSecurityToken); }
}

/// <summary>
/// Determines if the <see cref="ReadOnlyMemory{T}"/> is a well formed Json Web Token (JWT).
/// <para>See: https://datatracker.ietf.org/doc/html/rfc7519 </para>
/// </summary>
/// <param name="token"><see cref="ReadOnlyMemory{T}"/> that should represent a valid JWT.</param>
/// <remarks>Uses <see cref="Regex.IsMatch(string, string)"/> matching one of:
/// <para>JWS: @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$"</para>
/// <para>JWE: (dir): @"^[A-Za-z0-9-_]+\.\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$"</para>
/// <para>JWE: (wrappedkey): @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]$"</para>
/// </remarks>
/// <returns>
/// <para>'false' if the token is null or whitespace.</para>
/// <para>'false' if token.Length is greater than <see cref="TokenHandler.MaximumTokenSizeInBytes"/>.</para>
/// <para>'true' if the token is in JSON compact serialization format.</para>
/// </returns>
public virtual bool CanReadToken(in ReadOnlyMemory<char> token)
{
if (token.IsEmpty || token.Span.IsWhiteSpace())
return false;

if (token.Length > MaximumTokenSizeInBytes)
{
if (LogHelper.IsEnabled(EventLogLevel.Informational))
LogHelper.LogInformation(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes));

return false;
}

// Set the maximum number of segments to MaxJwtSegmentCount + 1. This controls the number of splits and allows detecting the number of segments is too large.
// For example: "a.b.c.d.e.f.g.h" => [a], [b], [c], [d], [e], [f.g.h]. 6 segments.
// If just MaxJwtSegmentCount was used, then [a], [b], [c], [d], [e.f.g.h] would be returned. 5 segments.
int tokenPartCount = JwtTokenUtilities.CountJwtTokenPart(token.Span, JwtConstants.MaxJwtSegmentCount + 1);
if (tokenPartCount == JwtConstants.JwsSegmentCount)
{
#if NET8_0_OR_GREATER
return JwtTokenUtilities.RegexJws.IsMatch(token.Span);
#else
return JwtTokenUtilities.RegexJws.IsMatch(token.ToString());
#endif
}
else if (tokenPartCount == JwtConstants.JweSegmentCount)
{
#if NET8_0_OR_GREATER
return JwtTokenUtilities.RegexJwe.IsMatch(token);
#else
return JwtTokenUtilities.RegexJwe.IsMatch(token.ToString());
#endif
}

LogHelper.LogInformation(LogMessages.IDX12720);
return false;
}

/// <summary>
/// Determines if the string is a well formed Json Web Token (JWT).
/// <para>See: https://datatracker.ietf.org/doc/html/rfc7519 </para>
Expand All @@ -283,6 +336,9 @@ public override bool CanReadToken(string token)
if (string.IsNullOrWhiteSpace(token))
return false;

#if NET8_0_OR_GREATER
return CanReadToken(token.AsMemory());
#else
if (token.Length > MaximumTokenSizeInBytes)
{
if (LogHelper.IsEnabled(EventLogLevel.Informational))
Expand All @@ -294,7 +350,7 @@ public override bool CanReadToken(string token)
// Set the maximum number of segments to MaxJwtSegmentCount + 1. This controls the number of splits and allows detecting the number of segments is too large.
// For example: "a.b.c.d.e.f.g.h" => [a], [b], [c], [d], [e], [f.g.h]. 6 segments.
// If just MaxJwtSegmentCount was used, then [a], [b], [c], [d], [e.f.g.h] would be returned. 5 segments.
int tokenPartCount = JwtTokenUtilities.CountJwtTokenPart(token, JwtConstants.MaxJwtSegmentCount + 1);
int tokenPartCount = JwtTokenUtilities.CountJwtTokenPart(token.AsSpan(), JwtConstants.MaxJwtSegmentCount + 1);
if (tokenPartCount == JwtConstants.JwsSegmentCount)
{
return JwtTokenUtilities.RegexJws.IsMatch(token);
Expand All @@ -306,6 +362,7 @@ public override bool CanReadToken(string token)

LogHelper.LogInformation(LogMessages.IDX12720);
return false;
#endif
}

/// <summary>
Expand Down Expand Up @@ -823,7 +880,7 @@ public override ClaimsPrincipal ValidateToken(string token, TokenValidationParam
if (token.Length > MaximumTokenSizeInBytes)
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes))));

int tokenPartCount = JwtTokenUtilities.CountJwtTokenPart(token, JwtConstants.MaxJwtSegmentCount + 1);
int tokenPartCount = JwtTokenUtilities.CountJwtTokenPart(token.AsSpan(), JwtConstants.MaxJwtSegmentCount + 1);

if (tokenPartCount != JwtConstants.JwsSegmentCount && tokenPartCount != JwtConstants.JweSegmentCount)
throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX12741));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
virtual System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.CanReadToken(in System.ReadOnlyMemory<char> token) -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,18 @@ public void SegmentCanRead(JwtTheoryData theoryData)
TestUtilities.AssertFailIfErrors(context);
}

[Theory, MemberData(nameof(SegmentTheoryData), DisableDiscoveryEnumeration = true)]
public void SegmentCanReadMemory(JwtTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.SegmentCanRead", theoryData);

var handler = new JsonWebTokenHandler();
if (theoryData.CanRead != handler.CanReadToken(theoryData.Token.AsMemory()))
context.Diffs.Add("theoryData.CanRead != handler.CanReadToken(theoryData.Token.AsMemory()))");

TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<JwtTheoryData> SegmentTheoryData()
{
var theoryData = new TheoryData<JwtTheoryData>();
Expand Down Expand Up @@ -539,7 +551,7 @@ public static TheoryData<CreateTokenTheoryData> CreateJWEWithAesGcmTheoryData
}
}

// Tests checks to make sure that the token string created by the JsonWebTokenHandler is consistent with the
// Tests checks to make sure that the token string created by the JsonWebTokenHandler is consistent with the
// token string created by the JwtSecurityTokenHandler.
[Theory, MemberData(nameof(CreateJWETheoryData), DisableDiscoveryEnumeration = true)]
public async Task CreateJWE(CreateTokenTheoryData theoryData)
Expand Down Expand Up @@ -867,7 +879,7 @@ private static OpenIdConnectConfiguration CreateCustomConfigurationThatThrows(Se
return configurationWithCustomCryptoProviderFactory;
}

// Tests checks to make sure that the token string (JWE) created by calling
// Tests checks to make sure that the token string (JWE) created by calling
// CreateToken(string payload, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials)
// is equivalent to the token string created by calling CreateToken(SecurityTokenDescriptor tokenDescriptor).
[Theory, MemberData(nameof(CreateJWEUsingSecurityTokenDescriptorTheoryData), DisableDiscoveryEnumeration = true)]
Expand Down Expand Up @@ -1258,7 +1270,7 @@ public static TheoryData<CreateTokenTheoryData> CreateJWEUsingSecurityTokenDescr
}
}

// Tests checks to make sure that the token string created by the JsonWebTokenHandler is consistent with the
// Tests checks to make sure that the token string created by the JsonWebTokenHandler is consistent with the
// token string created by the JwtSecurityTokenHandler.
[Theory, MemberData(nameof(CreateJWSTheoryData), DisableDiscoveryEnumeration = true)]
public async Task CreateJWS(CreateTokenTheoryData theoryData)
Expand Down Expand Up @@ -2053,7 +2065,7 @@ public static TheoryData<CreateTokenTheoryData> CreateJWSUsingSecurityTokenDescr
{
// Test checks that the values in SecurityTokenDescriptor.Subject.Claims
// are properly combined with those specified in SecurityTokenDescriptor.Claims.
// Duplicate values (if present with different case) should not be overridden.
// Duplicate values (if present with different case) should not be overridden.
// For example, the 'aud' claim on TokenDescriptor.Claims will not be overridden
// by the 'AUD' claim on TokenDescriptor.Subject.Claims, but the 'exp' claim will.
new CreateTokenTheoryData("TokenDescriptorWithBothSubjectAndClaims")
Expand Down Expand Up @@ -2513,7 +2525,7 @@ public async Task AdditionalHeaderValues()


// Test checks to make sure that the token payload retrieved from ValidateToken is the same as the payload
// the token was initially created with.
// the token was initially created with.
[Fact]
public async Task RoundTripJWS()
{
Expand Down Expand Up @@ -3210,7 +3222,7 @@ public async Task ValidateJsonWebTokenClaimMapping()
TestUtilities.AssertFailIfErrors(context);
}

// Test shows if the JwtSecurityTokenHandler has mapping OFF and
// Test shows if the JwtSecurityTokenHandler has mapping OFF and
// the JsonWebTokenHandler has mapping ON,the claims are different.
[Fact]
public async Task ValidateDifferentClaimsBetweenHandlers()
Expand Down
Loading