diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 2a722a0cbd..1968da629d 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -120,6 +120,25 @@ public IDictionary InboundClaimTypeMap } } + /// + /// Determines if the is a well formed JSON Web Token (JWT). See: . + /// + /// that should represent a valid JWT. + /// Uses matching: + /// JWS: @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$" + /// JWE: (dir): @"^[A-Za-z0-9-_]+\.\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$" + /// JWE: (wrappedkey): @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]$" + /// + /// + /// if the token is null or whitespace. + /// if token.Length is greater than . + /// if the token is in JSON Compact Serialization format. + /// + public virtual bool CanReadToken(ReadOnlyMemory token) + { + return JwtTokenUtilities.CanReadToken(token, MaximumTokenSizeInBytes); + } + /// /// Determines if the string is a well formed JSON Web Token (JWT). See: . /// @@ -139,6 +158,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)) @@ -169,6 +191,7 @@ public virtual bool CanReadToken(string token) LogHelper.LogInformation(LogMessages.IDX14107); return false; } +#endif } private static StringComparison GetStringComparisonRuleIf509(SecurityKey securityKey) => diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs index ff7d7a01b2..e07a212f27 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs @@ -564,33 +564,87 @@ internal static SecurityKey ResolveTokenSigningKey(string kid, string x5t, IEnum return null; } + /// + /// Determines if the token can be read. + /// + /// The token to check. + /// Maximum allowed size of the token, method will return false if exceeded. + internal static bool CanReadToken(ReadOnlyMemory token, int maximumTokenSizeInBytes) + { + 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 segmentCount = CountJwtTokenPart(token.Span, JwtConstants.MaxJwtSegmentCount + 1); + + switch (segmentCount) + { + 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; + } + } + /// /// Counts the number of JWT token segments. /// /// The JWT token. /// The maximum number of segments to count up to. /// The number of segments up to . - internal static int CountJwtTokenPart(string token, int maxCount) + internal static int CountJwtTokenPart(ReadOnlySpan token, int maxCount) { - var count = 1; - var index = 0; - while (index < token.Length) + int count = 1; + ReadOnlySpan localToken = token; + while (localToken.Length > 0) { - var dotIndex = token.IndexOf('.', index); + int dotIndex = localToken.IndexOf('.'); if (dotIndex < 0) - { break; - } count++; - index = dotIndex + 1; if (count == maxCount) - { break; - } + localToken = localToken.Slice(dotIndex + 1); } + return count; } + /// + /// Counts the number of JWT token segments. + /// + /// + /// This method is kept for backward compatibility when using mixed package versions + /// and must not be called directly in new code. + /// + /// The JWT token. + /// The maximum number of segments to count up to. + /// The number of segments up to . + internal static int CountJwtTokenPart(string token, int maxCount) => CountJwtTokenPart(token.AsSpan(), maxCount); + internal static IEnumerable ConcatSigningKeys(TokenValidationParameters tvp) { if (tvp == null) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index aed7d583d3..e092b13c68 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ -Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DecryptTokenWithConfigurationAsync(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DecryptTokenWithConfigurationAsync(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +virtual Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CanReadToken(System.ReadOnlyMemory token) -> bool diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index e5ad5cc095..b0204bdba2 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -263,6 +263,26 @@ public override Type TokenType get { return typeof(JwtSecurityToken); } } + /// + /// Determines if the is a well formed Json Web Token (JWT). + /// See: https://datatracker.ietf.org/doc/html/rfc7519 + /// + /// that should represent a valid JWT. + /// Uses matching one of: + /// JWS: @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$" + /// JWE: (dir): @"^[A-Za-z0-9-_]+\.\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$" + /// JWE: (wrappedkey): @"^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]$" + /// + /// + /// 'false' if the token is null or whitespace. + /// 'false' if token.Length is greater than . + /// 'true' if the token is in JSON compact serialization format. + /// + public virtual bool CanReadToken(ReadOnlyMemory token) + { + return JwtTokenUtilities.CanReadToken(token, MaximumTokenSizeInBytes); + } + /// /// Determines if the string is a well formed Json Web Token (JWT). /// See: https://datatracker.ietf.org/doc/html/rfc7519 @@ -283,6 +303,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)) @@ -294,7 +317,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); @@ -306,6 +329,7 @@ public override bool CanReadToken(string token) LogHelper.LogInformation(LogMessages.IDX12720); return false; +#endif } /// @@ -823,7 +847,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)); diff --git a/src/System.IdentityModel.Tokens.Jwt/PublicAPI.Unshipped.txt b/src/System.IdentityModel.Tokens.Jwt/PublicAPI.Unshipped.txt index e69de29bb2..59fdf9838d 100644 --- a/src/System.IdentityModel.Tokens.Jwt/PublicAPI.Unshipped.txt +++ b/src/System.IdentityModel.Tokens.Jwt/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +virtual System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.CanReadToken(System.ReadOnlyMemory token) -> bool \ No newline at end of file diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs index f9aa459cb2..ab60960e33 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs @@ -259,6 +259,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 SegmentTheoryData() { var theoryData = new TheoryData(); @@ -540,7 +552,7 @@ public static TheoryData 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) @@ -868,7 +880,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)] @@ -1259,7 +1271,7 @@ public static TheoryData 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) @@ -2054,7 +2066,7 @@ public static TheoryData 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") @@ -2514,7 +2526,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() { @@ -3211,7 +3223,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() diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/CrossTokenTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/CrossTokenTests.cs index 2498c9ecf5..9ac9f065fb 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/CrossTokenTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/CrossTokenTests.cs @@ -89,14 +89,25 @@ public void CanReadToken(TokenHandlerTheoryData theoryData) { var tokenHandler = theoryData.TokenHandler; - if (tokenHandler is SecurityTokenHandler securityTokenHandler) + if (tokenHandler is JwtSecurityTokenHandler jwtSecurityTokenHandler) + { + bool canReadToken = theoryData.UseMemoryOverload + ? jwtSecurityTokenHandler.CanReadToken(theoryData.Token.AsMemory()) + : jwtSecurityTokenHandler.CanReadToken(theoryData.Token); + if (canReadToken != theoryData.CanReadToken) + context.AddDiff("jwtSecurityTokenHandler.CanReadToken(theoryData.Token) != theoryData.CanReadToken"); + } + else if (tokenHandler is SecurityTokenHandler securityTokenHandler) { if (securityTokenHandler.CanReadToken(theoryData.Token) != theoryData.CanReadToken) context.AddDiff("securityTokenHandler.CanReadToken(theoryData.Token) != theoryData.CanReadToken"); } else if (tokenHandler is JsonWebTokenHandler jsonWebTokenHandler) { - if (jsonWebTokenHandler.CanReadToken(theoryData.Token) != theoryData.CanReadToken) + bool canReadToken = theoryData.UseMemoryOverload + ? jsonWebTokenHandler.CanReadToken(theoryData.Token.AsMemory()) + : jsonWebTokenHandler.CanReadToken(theoryData.Token); + if (canReadToken != theoryData.CanReadToken) context.AddDiff("jsonWebTokenHandler.CanReadToken(theoryData.Token) != theoryData.CanReadToken"); } else @@ -129,7 +140,18 @@ public static TheoryData CanReadTokenTheoryData Token = Default.AsymmetricJwt, CanReadToken = true, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "ValidJwt" + TestId = "ValidJwt", + UseMemoryOverload = false + }, + new TokenHandlerTheoryData + { + First = true, + TokenHandler = new JwtSecurityTokenHandler(), + Token = Default.AsymmetricJwt, + CanReadToken = true, + ExpectedException = ExpectedException.NoExceptionExpected, + TestId = "ValidJwt", + UseMemoryOverload = true }, new TokenHandlerTheoryData { @@ -137,7 +159,26 @@ public static TheoryData CanReadTokenTheoryData Token = largeToken, CanReadToken = false, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "TokenTooLargeJwt" + TestId = "TokenTooLargeJwt", + UseMemoryOverload = false + }, + new TokenHandlerTheoryData + { + TokenHandler = new JwtSecurityTokenHandler(), + Token = largeToken, + CanReadToken = false, + ExpectedException = ExpectedException.NoExceptionExpected, + TestId = "TokenTooLargeJwt", + UseMemoryOverload = true + }, + new TokenHandlerTheoryData + { + TokenHandler = new JsonWebTokenHandler(), + Token = Default.AsymmetricJwt, + CanReadToken = true, + ExpectedException = ExpectedException.NoExceptionExpected, + TestId = "ValidJsonWebToken", + UseMemoryOverload = false }, new TokenHandlerTheoryData { @@ -145,7 +186,17 @@ public static TheoryData CanReadTokenTheoryData Token = Default.AsymmetricJwt, CanReadToken = true, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "ValidJsonWebToken" + TestId = "ValidJsonWebToken", + UseMemoryOverload = true + }, + new TokenHandlerTheoryData + { + TokenHandler = new JsonWebTokenHandler(), + Token = largeToken, + CanReadToken = false, + ExpectedException = ExpectedException.NoExceptionExpected, + TestId = "TokenTooLargeJsonWebToken", + UseMemoryOverload = false }, new TokenHandlerTheoryData { @@ -153,7 +204,8 @@ public static TheoryData CanReadTokenTheoryData Token = largeToken, CanReadToken = false, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "TokenTooLargeJsonWebToken" + TestId = "TokenTooLargeJsonWebToken", + UseMemoryOverload = true }, new TokenHandlerTheoryData { @@ -161,7 +213,8 @@ public static TheoryData CanReadTokenTheoryData Token = ReferenceTokens.SamlToken_Valid, CanReadToken = true, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "ValidSaml1" + TestId = "ValidSaml1", + UseMemoryOverload = false }, new TokenHandlerTheoryData { @@ -169,7 +222,8 @@ public static TheoryData CanReadTokenTheoryData Token = largeToken, CanReadToken = false, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "TokenTooLargeSaml1" + TestId = "TokenTooLargeSaml1", + UseMemoryOverload = false }, new TokenHandlerTheoryData { @@ -177,7 +231,8 @@ public static TheoryData CanReadTokenTheoryData Token = ReferenceTokens.Saml2Token_Valid, CanReadToken = true, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "ValidSaml2" + TestId = "ValidSaml2", + UseMemoryOverload = false }, new TokenHandlerTheoryData { @@ -185,7 +240,8 @@ public static TheoryData CanReadTokenTheoryData Token = largeToken, CanReadToken = false, ExpectedException = ExpectedException.NoExceptionExpected, - TestId = "TokenTooLargeSaml2" + TestId = "TokenTooLargeSaml2", + UseMemoryOverload = false }, }; } @@ -216,6 +272,7 @@ public class TokenHandlerTheoryData : TheoryDataBase public TokenHandler TokenHandler { get; set; } public string Token { get; set; } + public bool UseMemoryOverload { get; set; } } }