From 1b2845ac6ac16a69db286b7c2c46d0d4a1ef1f14 Mon Sep 17 00:00:00 2001 From: Sruthi Keerthi Rangavajhula Date: Wed, 24 Jan 2024 13:33:20 -0800 Subject: [PATCH] Add JWT ctor that accepts a span Update YAML to build previews Add ReadOnlySpan overloads Add benchmark Add ValidateAndGetOutputSize for spans Remove duplication Update benchmark Remove duplicates Update Add benchmarks Update Replace ReadyOnlySpan with ReadOnlyMemory Update test Update benchmark naming Move ArrayPool to JsonWebToken level Update CreateHeaderClaimSet Update benchmark for JWE to resolve error Fix a typo (#2479) * update comment for azp in jsonwebtoken * removed extra words Link to breaking change announcement in IDX10506 (#2478) When an IDX10506 exception is thrown from JsonWebTokenHandler, there's a good chance this is due to a breaking change to ASP.NET Core 8. This adds a link to the breaking change announcement at https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events 7.3.1 changelog (#2476) * 7.3.1 changelog * two additions * add space fix log message dup (#2481) Reduce duplication Fix comment Set _encodedTokenMemory as readonly Remove unnecessary private method Use Base64UrlEncoding.Decode with action Use ValidateAndGetOutputSize(ReadOnlySpan strSpan..) thoughout Add tests Revert change Temporarily allow publishing to NuGet use just `1` for preview up version to 7.4.0 for preview Separate JWS and JWE benchmarks --- CHANGELOG.md | 14 ++ .../Program.cs | 4 + .../ReadJWETokenTests.cs | 47 +++++ .../ReadJWSTokenTests.cs | 47 +++++ build/releaseBuild.yml | 10 +- buildConfiguration.xml | 2 +- .../Json/JsonWebToken.HeaderClaimSet.cs | 9 +- .../Json/JsonWebToken.PayloadClaimSet.cs | 7 +- .../JsonWebToken.cs | 192 ++++++++++++------ .../JsonWebTokenHandler.cs | 10 +- .../LogMessages.cs | 7 +- .../Base64UrlEncoder.cs | 38 ++-- .../Base64UrlEncoding.cs | 69 ++++--- .../LogMessages.cs | 3 +- .../JsonWebTokenTests.cs | 44 ++++ .../Base64UrlEncodingTests.cs | 20 ++ .../SignatureProviderTests.cs | 1 + .../References.cs | 6 +- updateAssemblyInfo.ps1 | 2 +- 19 files changed, 396 insertions(+), 136 deletions(-) create mode 100644 benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWETokenTests.cs create mode 100644 benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWSTokenTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d04985a0f7..0d7d33a1c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ See the [releases](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases) for details on bug fixes and added features. +7.3.1 +====== +### Bug Fixes: +- Replace propertyName with `MetadataName` constant. See issue [#2471](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2471) for details. +- Fix 6x to 7x regression where mixed cases OIDC json was not correctly process. See [#2404](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2402) and [#2402](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2402) for details. + +### Performance Improvements: +- Update the benchmark configuration. See issue [#2468](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2468). + +### Documentation: +- Update comment for `azp` in `JsonWebToken`. See [#2475](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2475) for details. +- Link to breaking change announcement. See [#2478]. +- Fix typo in log message. See [#2479]. + 7.3.0 ====== ### New Features: diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/Program.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/Program.cs index d02f609cf8..785f31b22b 100644 --- a/benchmark/Microsoft.IdentityModel.Benchmarks/Program.cs +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/Program.cs @@ -24,6 +24,10 @@ public static void Main(string[] args) } private static void DebugThroughTests() { + ReadJWETokenTests readTokenTests = new ReadJWETokenTests(); + readTokenTests.Setup(); + readTokenTests.ReadJWE_FromMemory(); + AsymmetricAdapterSignatures asymmetricAdapter = new AsymmetricAdapterSignatures(); asymmetricAdapter.Setup(); asymmetricAdapter.SignDotnetCreatingBufferRSA(); diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWETokenTests.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWETokenTests.cs new file mode 100644 index 0000000000..a6a3bf4d02 --- /dev/null +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWETokenTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using BenchmarkDotNet.Attributes; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.IdentityModel.Benchmarks +{ + // dotnet run -c release -f net8.0 --filter Microsoft.IdentityModel.Benchmarks.ReadTokenTests* + + [Config(typeof(BenchmarkConfig))] + [HideColumns("Type", "Job", "WarmupCount", "LaunchCount")] + [MemoryDiagnoser] + public class ReadJWETokenTests + { + string _encryptedJWE; + + [GlobalSetup] + public void Setup() + { + var jsonWebTokenHandler = new JsonWebTokenHandler(); + var jweTokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256, + EncryptingCredentials = BenchmarkUtils.EncryptingCredentialsAes256Sha512, + TokenType = JwtHeaderParameterNames.Jwk, + Claims = BenchmarkUtils.Claims + }; + + _encryptedJWE = jsonWebTokenHandler.CreateToken(jweTokenDescriptor); + } + + [Benchmark] + public JsonWebToken ReadJWE_FromString() + { + return new JsonWebToken(_encryptedJWE); + } + + [Benchmark] + public JsonWebToken ReadJWE_FromMemory() + { + return new JsonWebToken(_encryptedJWE.AsMemory()); + } + } +} diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWSTokenTests.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWSTokenTests.cs new file mode 100644 index 0000000000..5ffc11c95d --- /dev/null +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/ReadJWSTokenTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using BenchmarkDotNet.Attributes; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.IdentityModel.Benchmarks +{ + // dotnet run -c release -f net8.0 --filter Microsoft.IdentityModel.Benchmarks.ReadJWSTokenTests* + + [Config(typeof(BenchmarkConfig))] + [HideColumns("Type", "Job", "WarmupCount", "LaunchCount")] + [MemoryDiagnoser] + [RankColumn] + public class ReadJWSTokenTests + { + string _encodedJWS; + + [GlobalSetup] + public void Setup() + { + var jsonWebTokenHandler = new JsonWebTokenHandler(); + var jwsTokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256, + TokenType = JwtHeaderParameterNames.Jwk, + Claims = BenchmarkUtils.Claims + }; + + _encodedJWS = jsonWebTokenHandler.CreateToken(jwsTokenDescriptor); + } + + [Benchmark] + public JsonWebToken ReadJWS_FromString() + { + return new JsonWebToken(_encodedJWS); + } + + [Benchmark] + public JsonWebToken ReadJWS_FromMemory() + { + return new JsonWebToken(_encodedJWS.AsMemory()); + } + } +} diff --git a/build/releaseBuild.yml b/build/releaseBuild.yml index 9bb207b208..b020d5bce1 100644 --- a/build/releaseBuild.yml +++ b/build/releaseBuild.yml @@ -68,7 +68,7 @@ jobs: inputs: targetType: filePath filePath: ./updateAssemblyInfo.ps1 - arguments: '-packageType $(BuildConfiguration)' + arguments: '-packageType $(NugetPackageType)' - task: DotNetCoreCLI@2 displayName: Build @@ -195,6 +195,14 @@ jobs: BuildDropPath: '$(Build.SourcesDirectory)\src' ManifestDirPath: '$(Build.SourcesDirectory)\artifacts' + - task: NuGetCommand@2 + displayName: 'Upload NuGet Package to VSTS NuGet' + inputs: + command: push + packagesToPush: '$(Build.Repository.LocalPath)\artifacts\*.nupkg' + publishVstsFeed: '46419298-b96c-437f-bd4c-12c8df7f868d' + allowPackageConflicts: true + - task: PublishBuildArtifacts@1 displayName: 'Publish NuGet Package Artifact' inputs: diff --git a/buildConfiguration.xml b/buildConfiguration.xml index 213a1a2a58..9f73fc9582 100644 --- a/buildConfiguration.xml +++ b/buildConfiguration.xml @@ -2,7 +2,7 @@ x64 3.5.0-rc-1285 net461,netstandard2.0 - 7.3.1 + 7.4.0 preview diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs index 5279e34322..ff3ab79cb1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs @@ -13,12 +13,17 @@ public partial class JsonWebToken { internal JsonClaimSet CreateHeaderClaimSet(byte[] bytes) { - return CreateHeaderClaimSet(bytes, bytes.Length); + return CreateHeaderClaimSet(bytes.AsSpan()); } internal JsonClaimSet CreateHeaderClaimSet(byte[] bytes, int length) { - Utf8JsonReader reader = new(bytes.AsSpan().Slice(0, length)); + return CreateHeaderClaimSet(bytes.AsSpan(0, length)); + } + + internal JsonClaimSet CreateHeaderClaimSet(ReadOnlySpan byteSpan) + { + Utf8JsonReader reader = new(byteSpan); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, false)) throw LogHelper.LogExceptionMessage( new JsonException( diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 24e19225c8..aad919b03a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -14,7 +14,12 @@ public partial class JsonWebToken { internal JsonClaimSet CreatePayloadClaimSet(byte[] bytes, int length) { - Utf8JsonReader reader = new(bytes.AsSpan().Slice(0, length)); + return CreatePayloadClaimSet(bytes.AsSpan(0, length)); + } + + internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) + { + Utf8JsonReader reader = new(byteSpan); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, false)) throw LogHelper.LogExceptionMessage( new JsonException( diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 58652ff45b..a8c1c38376 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Buffers; using System.Collections.Generic; using System.Security.Claims; using System.Text; @@ -27,9 +28,11 @@ public partial class JsonWebToken : SecurityToken private string _encodedHeader; private string _encodedPayload; private string _encodedSignature; + private string _encodedToken; private string _encryptedKey; private string _initializationVector; private List _audiences; + private readonly ReadOnlyMemory _encodedTokenMemory; #region properties relating to the header // when constructing a JWT, these properties, when found, will be set @@ -78,7 +81,33 @@ public JsonWebToken(string jwtEncodedString) if (string.IsNullOrEmpty(jwtEncodedString)) throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(jwtEncodedString))); - ReadToken(jwtEncodedString); + ReadToken(jwtEncodedString.AsMemory()); + + _encodedToken = jwtEncodedString; + } + + /// + /// Initializes a new instance of from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format. + /// + /// A ReadOnlyMemory{char} containing the JSON Web Token serialized in JWS or JWE Compact format. + /// Thrown when is empty. + /// Thrown when does not represent a valid JWS or JWE Compact serialization format. + /// + /// See: https://datatracker.ietf.org/doc/html/rfc7519 (JWT) + /// See: https://datatracker.ietf.org/doc/html/rfc7515 (JWS) + /// See: https://datatracker.ietf.org/doc/html/rfc7516 (JWE) + /// + /// The contents of the returned have not been validated; the JSON Web Token is simply decoded. Validation can be performed using the methods in . + /// + /// + public JsonWebToken(ReadOnlyMemory encodedTokenMemory) + { + if (encodedTokenMemory.IsEmpty) + throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(encodedTokenMemory))); + + ReadToken(encodedTokenMemory); + + _encodedTokenMemory = encodedTokenMemory; } /// @@ -108,7 +137,9 @@ public JsonWebToken(string header, string payload) var encodedPayload = Base64UrlEncoder.Encode(payload); var encodedToken = encodedHeader + "." + encodedPayload + "."; - ReadToken(encodedToken); + ReadToken(encodedToken.AsMemory()); + + _encodedToken = encodedToken; } internal string ActualIssuer { get; set; } @@ -192,10 +223,10 @@ public string EncodedHeader // TODO - need to account for JWE if (_encodedHeader == null) { - if (EncodedToken != null) - _encodedHeader = EncodedToken.Substring(0, Dot1); + if (!_encodedTokenMemory.IsEmpty) + _encodedHeader = _encodedTokenMemory.Span.Slice(0, Dot1).ToString(); else - _encodedHeader = string.Empty; + _encodedHeader = (_encodedToken is not null) ? _encodedToken.Substring(0, Dot1) : string.Empty; } return _encodedHeader; @@ -237,10 +268,17 @@ public string EncodedPayload { if (_encodedPayload == null) { - if (EncodedToken != null) - _encodedPayload = IsEncrypted ? string.Empty : EncodedToken.Substring(Dot1 + 1, Dot2 - Dot1 - 1); + if (!_encodedTokenMemory.IsEmpty) + { + _encodedPayload = IsEncrypted ? string.Empty : _encodedTokenMemory.Span.Slice(Dot1 + 1, Dot2 - Dot1 - 1).ToString(); + } else - _encodedPayload = string.Empty; + { + if (_encodedToken is not null) + _encodedPayload = IsEncrypted ? string.Empty : _encodedToken.Substring(Dot1 + 1, Dot2 - Dot1 - 1); + else + _encodedPayload = string.Empty; + } } return _encodedPayload; @@ -260,10 +298,17 @@ public string EncodedSignature { if (_encodedSignature == null) { - if (EncodedToken != null) - _encodedSignature = IsEncrypted ? string.Empty : EncodedToken.Substring(Dot2 + 1, EncodedToken.Length - Dot2 - 1); + if (!_encodedTokenMemory.IsEmpty) + { + _encodedSignature = IsEncrypted ? string.Empty : _encodedTokenMemory.Span.Slice(Dot2 + 1, _encodedTokenMemory.Length - Dot2 - 1).ToString(); + } else - _encodedSignature = string.Empty; + { + if (_encodedToken is not null) + _encodedSignature = IsEncrypted ? string.Empty : _encodedToken.Substring(Dot2 + 1, _encodedToken.Length - Dot2 - 1); + else + _encodedSignature = string.Empty; + } } return _encodedSignature; @@ -276,7 +321,16 @@ public string EncodedSignature /// /// The original Base64UrlEncoded of the JWT. /// - public string EncodedToken { get; private set; } + public string EncodedToken + { + get + { + if (_encodedToken is null && !_encodedTokenMemory.IsEmpty) + _encodedToken = _encodedTokenMemory.ToString(); + + return _encodedToken; + } + } internal JsonClaimSet Header { get; set; } @@ -347,27 +401,27 @@ public string InitializationVector internal int NumberOfDots { get; set; } /// - /// Converts a string into an instance of . + /// Converts a span into an instance of . /// - /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. - /// if is malformed, a valid JWT should have either 2 dots (JWS) or 4 dots (JWE). - /// if does not have an non-empty authentication tag after the 4th dot for a JWE. - /// if has more than 4 dots. - internal void ReadToken(string encodedJson) + /// A span representing a 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. + /// if is malformed, a valid JWT should have either 2 dots (JWS) or 4 dots (JWE). + /// if does not have a non-empty authentication tag after the 4th dot for a JWE. + /// if has more than 4 dots. + internal void ReadToken(ReadOnlyMemory encodedTokenMemory) { - // JWT must have 2 dots - Dot1 = encodedJson.IndexOf('.'); - if (Dot1 == -1 || Dot1 == encodedJson.Length - 1) + // JWT must have 2 dots for JWS or 4 dots for JWE (a.b.c.d.e) + ReadOnlySpan encodedTokenSpan = encodedTokenMemory.Span; + + Dot1 = encodedTokenSpan.IndexOf('.'); + if (Dot1 == -1 || Dot1 == encodedTokenSpan.Length - 1) throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14100)); - Dot2 = encodedJson.IndexOf('.', Dot1 + 1); + Dot2 = encodedTokenSpan.Slice(Dot1 + 1).IndexOf('.'); if (Dot2 == -1) throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14120)); - if (Dot2 == encodedJson.Length - 1) - Dot3 = -1; - else - Dot3 = encodedJson.IndexOf('.', Dot2 + 1); + Dot2 = Dot1 + Dot2 + 1; + Dot3 = (Dot2 == encodedTokenSpan.Length - 1) ? -1 : encodedTokenSpan.Slice(Dot2 + 1).IndexOf('.'); if (Dot3 == -1) { @@ -375,28 +429,28 @@ internal void ReadToken(string encodedJson) // JWS: https://www.rfc-editor.org/rfc/rfc7515 // Format: https://www.rfc-editor.org/rfc/rfc7515#page-7 - IsSigned = !(Dot2 + 1 == encodedJson.Length); + IsSigned = !(Dot2 + 1 == encodedTokenSpan.Length); try { - Header = CreateClaimSet(encodedJson, 0, Dot1, CreateHeaderClaimSet); + Header = CreateClaimSet(encodedTokenSpan, 0, Dot1, createHeaderClaimSet: true); } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant( LogMessages.IDX14102, - LogHelper.MarkAsUnsafeSecurityArtifact(encodedJson.Substring(0, Dot1), t => t.ToString())), + LogHelper.MarkAsUnsafeSecurityArtifact(encodedTokenSpan.Slice(0, Dot1).ToString(), t => t.ToString())), // TODO: Add an overload to LogHelper.MarkAsUnsafeSecurityArtifact that accepts span? ex)); } try { - Payload = CreateClaimSet(encodedJson, Dot1 + 1, Dot2 - Dot1 - 1, CreatePayloadClaimSet); + Payload = CreateClaimSet(encodedTokenSpan, Dot1 + 1, Dot2 - Dot1 - 1, createHeaderClaimSet: false); } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant( LogMessages.IDX14101, - LogHelper.MarkAsUnsafeSecurityArtifact(encodedJson.Substring(Dot1 + 1, Dot2 - Dot1 - 1), t => t.ToString())), + LogHelper.MarkAsUnsafeSecurityArtifact(encodedTokenSpan.Slice(Dot1 + 1, Dot2 - Dot1 - 1).ToString(), t => t.ToString())), ex)); } } @@ -407,115 +461,123 @@ internal void ReadToken(string encodedJson) // empty payload for JWE's {encrypted tokens}. Payload = new JsonClaimSet(); - if (Dot3 == encodedJson.Length) + if (Dot3 == encodedTokenSpan.Length) // TODO: Should this be encodedJsonSpan.Length - 1? throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14121)); - Dot4 = encodedJson.IndexOf('.', Dot3 + 1); + Dot3 = Dot2 + Dot3 + 1; - // JWE needs to have 4 dots + Dot4 = encodedTokenSpan.Slice(Dot3 + 1).IndexOf('.'); if (Dot4 == -1) throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14121)); - - // too many dots... - if (encodedJson.IndexOf('.', Dot4 + 1) != -1) - throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14122)); + + Dot4 = Dot3 + Dot4 + 1; // must have something after 4th dot - if (Dot4 == encodedJson.Length - 1) + if (Dot4 == encodedTokenSpan.Length - 1) throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14310)); - // right number of dots for JWE - ReadOnlyMemory hChars = encodedJson.AsMemory(0, Dot1); + if (encodedTokenSpan.Slice(Dot4 + 1).IndexOf('.') != -1) + throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14122)); - // header cannot be empty - if (hChars.IsEmpty) + ReadOnlySpan headerSpan = encodedTokenSpan.Slice(0, Dot1); + if (headerSpan.IsEmpty) throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14307)); - byte[] headerAsciiBytes = new byte[hChars.Length]; + // right number of dots for JWE (4) + byte[] headerAsciiBytes = new byte[headerSpan.Length]; #if NET6_0_OR_GREATER - Encoding.ASCII.GetBytes(hChars.Span, headerAsciiBytes); + Encoding.ASCII.GetBytes(headerSpan, headerAsciiBytes); #else unsafe { - fixed (char* hCharsPtr = hChars.Span) + fixed (char* hCharsPtr = headerSpan) fixed (byte* headerAsciiBytesPtr = headerAsciiBytes) { - Encoding.ASCII.GetBytes(hCharsPtr, hChars.Length, headerAsciiBytesPtr, headerAsciiBytes.Length); + Encoding.ASCII.GetBytes(hCharsPtr, headerSpan.Length, headerAsciiBytesPtr, headerAsciiBytes.Length); } } #endif + HeaderAsciiBytes = headerAsciiBytes; try { - Header = CreateHeaderClaimSet(Base64UrlEncoder.UnsafeDecode(hChars)); + Header = CreateHeaderClaimSet(Base64UrlEncoder.UnsafeDecode(headerSpan).AsSpan()); } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant( LogMessages.IDX14102, - LogHelper.MarkAsUnsafeSecurityArtifact(encodedJson.Substring(0, Dot1), t => t.ToString())), + LogHelper.MarkAsUnsafeSecurityArtifact(headerSpan.ToString(), t => t.ToString())), ex)); } - // dir does not have any key bytes - ReadOnlyMemory encryptedKeyBytes = encodedJson.AsMemory(Dot1 + 1, Dot2 - Dot1 - 1); + // delegating retrieving encrypted Key to the getter on EncryptedKey + ReadOnlySpan encryptedKeyBytes = encodedTokenSpan.Slice(Dot1 + 1, Dot2 - Dot1 - 1); if (!encryptedKeyBytes.IsEmpty) { EncryptedKeyBytes = Base64UrlEncoder.UnsafeDecode(encryptedKeyBytes); - _encryptedKey = encodedJson.Substring(Dot1 + 1, Dot2 - Dot1 - 1); + _encryptedKey = encodedTokenSpan.Slice(Dot1 + 1, Dot2 - Dot1 - 1).ToString(); } else { _encryptedKey = string.Empty; } - ReadOnlyMemory initializationVectorChars = encodedJson.AsMemory(Dot2 + 1, Dot3 - Dot2 - 1); - if (initializationVectorChars.IsEmpty) + ReadOnlySpan initializationVectorSpan = encodedTokenSpan.Slice(Dot2 + 1, Dot3 - Dot2 - 1); + if (initializationVectorSpan.IsEmpty) throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14308)); try { - InitializationVectorBytes = Base64UrlEncoder.UnsafeDecode(initializationVectorChars); + InitializationVectorBytes = Base64UrlEncoder.UnsafeDecode(initializationVectorSpan); } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14309, ex)); } - ReadOnlyMemory authTagChars = encodedJson.AsMemory(Dot4 + 1); - if (authTagChars.IsEmpty) + ReadOnlySpan authTagSpan = encodedTokenSpan.Slice(Dot4 + 1); + if (authTagSpan.IsEmpty) throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14310)); try { - AuthenticationTagBytes = Base64UrlEncoder.UnsafeDecode(authTagChars); + AuthenticationTagBytes = Base64UrlEncoder.UnsafeDecode(authTagSpan); } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14311, ex)); } - ReadOnlyMemory cipherTextBytes = encodedJson.AsMemory(Dot3 + 1, Dot4 - Dot3 - 1); - if (cipherTextBytes.IsEmpty) + ReadOnlySpan cipherTextSpan = encodedTokenSpan.Slice(Dot3 + 1, Dot4 - Dot3 - 1); + if (cipherTextSpan.IsEmpty) throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14306)); try { - CipherTextBytes = Base64UrlEncoder.UnsafeDecode(cipherTextBytes); + CipherTextBytes = Base64UrlEncoder.UnsafeDecode(cipherTextSpan); } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogMessages.IDX14312, ex)); } } - - EncodedToken = encodedJson; } - internal static JsonClaimSet CreateClaimSet(string rawString, int startIndex, int length, Func action) + internal JsonClaimSet CreateClaimSet(ReadOnlySpan strSpan, int startIndex, int length, bool createHeaderClaimSet) { - return Base64UrlEncoding.Decode(rawString, startIndex, length, action); + int outputSize = Base64UrlEncoding.ValidateAndGetOutputSize(strSpan, startIndex, length); + byte[] output = ArrayPool.Shared.Rent(outputSize); + try + { + Base64UrlEncoding.Decode(strSpan, startIndex, length, output); + return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsSpan()); + } + finally + { + ArrayPool.Shared.Return(output); + } } /// diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index c5d42bd84f..3d7882e8d6 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -17,7 +17,7 @@ namespace Microsoft.IdentityModel.JsonWebTokens { /// - /// A designed for creating and validating Json Web Tokens. + /// A designed for creating and validating Json Web Tokens. /// See: https://datatracker.ietf.org/doc/html/rfc7519 and http://www.rfc-editor.org/info/rfc7515. /// public partial class JsonWebTokenHandler : TokenHandler @@ -38,7 +38,7 @@ public partial class JsonWebTokenHandler : TokenHandler public static bool DefaultMapInboundClaims = false; /// - /// Gets the Base64Url encoded string representation of the following JWT header: + /// Gets the Base64Url encoded string representation of the following JWT header: /// { , }. /// /// The Base64Url encoded string representation of the unsigned JWT header. @@ -85,7 +85,7 @@ public static string ShortClaimTypeProperty } /// - /// Gets or sets the property which is used when determining whether or not to map claim types that are extracted when validating a . + /// Gets or sets the property which is used when determining whether or not to map claim types that are extracted when validating a . /// If this is set to true, the is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs. /// The default value is false. /// @@ -104,7 +104,7 @@ public bool MapInboundClaims } /// - /// Gets or sets the which is used when setting the for claims in the extracted when validating a . + /// Gets or sets the which is used when setting the for claims in the extracted when validating a . /// The is set to the JSON claim 'name' after translating using this mapping. /// The default value is ClaimTypeMapping.InboundClaimTypeMap. /// @@ -331,7 +331,7 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV } /// - /// Decrypts a JWE and returns the clear text + /// Decrypts a JWE and returns the clear text /// /// the JWE that contains the cypher text. /// contains crypto material. diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs index b7dd1b2519..12121de2f1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/LogMessages.cs @@ -11,8 +11,6 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// internal static class LogMessages { - #pragma warning disable 1591 - // signature creation / validation internal const string IDX14000 = "IDX14000: Signature validation of this JWT is not supported for: Algorithm: '{0}', SecurityKey: '{1}'."; @@ -32,14 +30,13 @@ internal static class LogMessages internal const string IDX14116 = "IDX14116: '{0}' cannot contain the following claims: '{1}'. These values are added by default (if necessary) during security token creation."; // number of sections 'dots' is not correct internal const string IDX14120 = "IDX14120: JWT is not well formed, there is only one dot (.).\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EndcodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; - internal const string IDX14121 = "IDX14121: JWT is not a well formed JWE, there are there must be four dots (.).\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EndcodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; + internal const string IDX14121 = "IDX14121: JWT is not a well formed JWE, there must be four dots (.).\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EndcodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; internal const string IDX14122 = "IDX14122: JWT is not a well formed JWE, there are more than four dots (.) a JWE can have at most 4 dots.\nThe token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EndcodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'."; // logging internal const string IDX14200 = "IDX14200: Creating raw signature using the signature credentials."; internal const string IDX14201 = "IDX14201: Creating raw signature using the signature credentials. Caching SignatureProvider: '{0}'."; - // parsing //internal const string IDX14300 = "IDX14300: Could not parse '{0}' : '{1}' as a '{2}'."; //internal const string IDX14301 = "IDX14301: Unable to parse the header into a JSON object. \nHeader: '{0}'."; @@ -54,7 +51,5 @@ internal static class LogMessages internal const string IDX14310 = "IDX14310: JWE authentication tag is missing."; internal const string IDX14311 = "IDX14311: Unable to decode the authentication tag as a Base64Url encoded string."; internal const string IDX14312 = "IDX14312: Unable to decode the cipher text as a Base64Url encoded string."; - - #pragma warning restore 1591 } } diff --git a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs index db6d1bc0d2..8a11a07c3c 100644 --- a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs +++ b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs @@ -173,20 +173,20 @@ public static int Encode(ReadOnlySpan inArray, Span output) public static byte[] DecodeBytes(string str) { _ = str ?? throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(str))); - return UnsafeDecode(str.AsMemory()); + return UnsafeDecode(str.AsSpan()); } #if NET6_0_OR_GREATER [SkipLocalsInit] #endif - internal static unsafe byte[] UnsafeDecode(ReadOnlyMemory str) + internal static unsafe byte[] UnsafeDecode(ReadOnlySpan strSpan) { - int mod = str.Length % 4; + int mod = strSpan.Length % 4; if (mod == 1) - throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, str.ToString()))); + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, strSpan.ToString()))); - bool needReplace = str.Span.IndexOfAny(base64UrlCharacter62, base64UrlCharacter63) >= 0; - int decodedLength = str.Length + (4 - mod) % 4; + bool needReplace = strSpan.IndexOfAny(base64UrlCharacter62, base64UrlCharacter63) >= 0; + int decodedLength = strSpan.Length + (4 - mod) % 4; #if NET6_0_OR_GREATER // If the incoming chars don't contain any of the base64url characters that need to be replaced, @@ -198,7 +198,7 @@ internal static unsafe byte[] UnsafeDecode(ReadOnlyMemory str) const int StackAllocThreshold = 512; char[] arrayPoolChars = null; scoped Span charsSpan = default; - scoped ReadOnlySpan source = str.Span; + scoped ReadOnlySpan source = strSpan; if (needReplace || decodedLength != source.Length) { @@ -256,7 +256,6 @@ internal static unsafe byte[] UnsafeDecode(ReadOnlyMemory str) #else if (needReplace) { - ReadOnlySpan strSpan = str.Span; string decodedString = new(char.MinValue, decodedLength); fixed (char* dest = decodedString) { @@ -279,30 +278,21 @@ internal static unsafe byte[] UnsafeDecode(ReadOnlyMemory str) } else { - if (decodedLength == str.Length) + if (decodedLength == strSpan.Length) { - if (MemoryMarshal.TryGetArray(str, out ArraySegment segment)) - { - return Convert.FromBase64CharArray(segment.Array, segment.Offset, segment.Count); - } - else - { - bool gotString = MemoryMarshal.TryGetString(str, out string text, out int start, out int length); - Debug.Assert(gotString, "Expected ReadOnlyMemory to wrap either array or string"); - return Convert.FromBase64String(text.Substring(start, length)); - } + return Convert.FromBase64CharArray(strSpan.ToArray(), 0, strSpan.Length); } else { string decodedString = new(char.MinValue, decodedLength); - fixed (char* src = str.Span) + fixed (char* src = strSpan) fixed (char* dest = decodedString) { - Buffer.MemoryCopy(src, dest, str.Length * 2, str.Length * 2); + Buffer.MemoryCopy(src, dest, strSpan.Length * 2, strSpan.Length * 2); - dest[str.Length] = base64PadCharacter; - if (str.Length + 2 == decodedLength) - dest[str.Length + 1] = base64PadCharacter; + dest[strSpan.Length] = base64PadCharacter; + if (strSpan.Length + 2 == decodedLength) + dest[strSpan.Length + 1] = base64PadCharacter; } return Convert.FromBase64String(decodedString); diff --git a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs index 4c5b6effe6..1cdbd0809f 100644 --- a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs +++ b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs @@ -57,9 +57,9 @@ public static byte[] Decode(string input, int offset, int length) { _ = input ?? throw LogHelper.LogArgumentNullException(nameof(input)); - int outputsize = ValidateAndGetOutputSize(input, offset, length); + int outputsize = ValidateAndGetOutputSize(input.AsSpan(), offset, length); byte[] output = new byte[outputsize]; - Decode(input, offset, length, output); + Decode(input.AsSpan(), offset, length, output); return output; } @@ -77,16 +77,16 @@ public static byte[] Decode(string input, int offset, int length) /// /// The buffer for the decode operation uses shared memory pool to avoid allocations. /// The length of the rented array of bytes may be larger than the decoded bytes, therefore the action needs to know the actual length to use. - /// The result of is passed to the action. + /// The result of is passed to the action. /// public static T Decode(string input, int offset, int length, TX argx, Func action) { _ = action ?? throw new ArgumentNullException(nameof(action)); - int outputsize = ValidateAndGetOutputSize(input, offset, length); + int outputsize = ValidateAndGetOutputSize(input.AsSpan(), offset, length); byte[] output = ArrayPool.Shared.Rent(outputsize); try { - Decode(input, offset, length, output); + Decode(input.AsSpan(), offset, length, output); return action(output, outputsize, argx); } finally @@ -107,7 +107,7 @@ public static T Decode(string input, int offset, int length, TX argx, Fun /// /// The buffer for the decode operation uses shared memory pool to avoid allocations. /// The length of the rented array of bytes may be larger than the decoded bytes, therefore the action needs to know the actual length to use. - /// The result of is passed to the action. + /// The result of is passed to the action. /// public static T Decode(string input, int offset, int length, Func action) { @@ -117,7 +117,7 @@ public static T Decode(string input, int offset, int length, Func.Shared.Rent(outputsize); try { - Decode(input, offset, length, output); + Decode(input.AsSpan(), offset, length, output); return action(output, outputsize); } finally @@ -144,7 +144,7 @@ public static T Decode(string input, int offset, int length, Func /// The buffer for the decode operation uses shared memory pool to avoid allocations. /// The length of the rented array of bytes may be larger than the decoded bytes, therefore the action needs to know the actual length to use. - /// The result of is passed to the action. + /// The result of is passed to the action. /// public static T Decode( string input, @@ -157,11 +157,11 @@ public static T Decode( { _ = action ?? throw LogHelper.LogArgumentNullException(nameof(action)); - int outputsize = ValidateAndGetOutputSize(input, offset, length); + int outputsize = ValidateAndGetOutputSize(input.AsSpan(), offset, length); byte[] output = ArrayPool.Shared.Rent(outputsize); try { - Decode(input, offset, length, output); + Decode(input.AsSpan(), offset, length, output); return action(output, outputsize, argx, argy, argz); } finally @@ -173,9 +173,9 @@ public static T Decode( /// /// Decodes a Base64UrlEncoded string into a byte array. /// - /// String to decode. + /// String represented as a span to decode. /// Index of char in to start decode operation. - /// Number of chars in to decode. + /// Number of chars beginning from to decode. /// byte array to place results. /// /// Changes from Base64UrlEncoder implementation @@ -183,7 +183,7 @@ public static T Decode( /// 2. '+' and '-' are treated the same. /// 3. '/' and '_' are treated the same. /// - private static void Decode(string input, int offset, int length, byte[] output) + internal static void Decode(ReadOnlySpan input, int offset, int length, byte[] output) { int outputpos = 0; uint curblock = 0x000000FFu; @@ -220,7 +220,7 @@ private static void Decode(string input, int offset, int length, byte[] output) LogHelper.FormatInvariant( LogMessages.IDX10820, LogHelper.MarkAsNonPII(cur), - input))); + input.ToString()))); } curblock = (curblock << 6) | cur; @@ -254,7 +254,7 @@ private static void Decode(string input, int offset, int length, byte[] output) else { throw LogHelper.LogExceptionMessage(new ArgumentException( - LogHelper.FormatInvariant(LogMessages.IDX10821, input))); + LogHelper.FormatInvariant(LogMessages.IDX10821, input.ToString()))); } } } @@ -335,8 +335,21 @@ public static string Encode(byte[] input, int offset, int length) private static int ValidateAndGetOutputSize(string inputString, int offset, int length) { _ = inputString ?? throw LogHelper.LogArgumentNullException(nameof(inputString)); - if (inputString.Length == 0) - return 0; + + return ValidateAndGetOutputSize(inputString.AsSpan(), offset, length); + } + + /// + /// Validates the input span for decode operation. + /// + /// String represented by a span to validate. + /// Index of char in to start decode operation. + /// Number of chars in to decode, starting from offset. + /// Size of the decoded bytes arrays. + internal static int ValidateAndGetOutputSize(ReadOnlySpan strSpan, int offset, int length) + { + if (strSpan.IsEmpty) + throw LogHelper.LogArgumentNullException(nameof(strSpan)); if (length == 0) return 0; @@ -355,37 +368,37 @@ private static int ValidateAndGetOutputSize(string inputString, int offset, int LogHelper.MarkAsNonPII(nameof(length)), LogHelper.MarkAsNonPII(length)))); - if (length + offset > inputString.Length) + if (length + offset > strSpan.Length) throw LogHelper.LogExceptionMessage(new ArgumentException( LogHelper.FormatInvariant( LogMessages.IDX10717, LogHelper.MarkAsNonPII(nameof(length)), LogHelper.MarkAsNonPII(nameof(offset)), - LogHelper.MarkAsNonPII(nameof(inputString)), + LogHelper.MarkAsNonPII(nameof(strSpan)), LogHelper.MarkAsNonPII(length), LogHelper.MarkAsNonPII(offset), - LogHelper.MarkAsNonPII(inputString.Length)))); + LogHelper.MarkAsNonPII(strSpan.Length)))); if (length % 4 == 1) - throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, inputString))); + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, strSpan.ToString()))); int lastCharPosition = offset + length - 1; // Compute useful length (i.e. ignore padding characters) - if (inputString[lastCharPosition] == '=') + if (strSpan[lastCharPosition] == '=') { lastCharPosition--; - if (inputString[lastCharPosition] == '=') + if (strSpan[lastCharPosition] == '=') lastCharPosition--; } int effectiveLength = 1 + (lastCharPosition - offset); - int outputsize = effectiveLength % 4; - if (outputsize > 0) - outputsize--; + int outputSize = effectiveLength % 4; + if (outputSize > 0) + outputSize--; - outputsize += (effectiveLength / 4) * 3; - return outputsize; + outputSize += (effectiveLength / 4) * 3; + return outputSize; } private static void WriteEncodedOutput(byte[] inputBytes, int offset, int length, Span output) diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 5f6a768328..7c5892ff3f 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -86,7 +86,8 @@ internal static class LogMessages public const string IDX10503 = "IDX10503: Signature validation failed. The token's kid is: '{0}', but did not match any keys in TokenValidationParameters or Configuration. Keys tried: '{1}'. Number of keys in TokenValidationParameters: '{2}'. \nNumber of keys in Configuration: '{3}'. \nExceptions caught:\n '{4}'.\ntoken: '{5}'. See https://aka.ms/IDX10503 for details."; public const string IDX10504 = "IDX10504: Unable to validate signature, token does not have a signature: '{0}'."; public const string IDX10505 = "IDX10505: Signature validation failed. The user defined 'Delegate' specified on TokenValidationParameters returned null when validating token: '{0}'."; - public const string IDX10506 = "IDX10506: Signature validation failed. The user defined 'Delegate' specified on TokenValidationParameters did not return a '{0}', but returned a '{1}' when validating token: '{2}'."; + // Provide a message more specific to JsonWebTokens while allowing people searching the ID to search solutions provided for the old message like those at https://stackoverflow.com/questions/77515249/custom-token-validator-not-working-in-net-8 + public const string IDX10506 = "IDX10506: Signature validation failed. The user defined 'Delegate' specified on TokenValidationParameters did not return a '{0}', but returned a '{1}' when validating token: '{2}'. If you are using ASP.NET Core 8 or later, see https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events for more details."; // public const string IDX10507 = "IDX10507:"; public const string IDX10508 = "IDX10508: Signature validation failed. Signature is improperly formatted."; public const string IDX10509 = "IDX10509: Token validation failed. The user defined 'Delegate' set on TokenValidationParameters.TokenReader did not return a '{0}', but returned a '{1}' when reading token: '{2}'."; diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 51c901c1e2..c0a2362b24 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -1598,6 +1598,30 @@ public static TheoryData ParseTokenTheoryData Token = EncodedJwts.JWEEmptyAuthenticationTag, }); + theoryData.Add(new JwtTheoryData(nameof(EncodedJwts.JWEInvalidHeader)) + { + ExpectedException = new ExpectedException(typeof(ArgumentException), "IDX14102:", typeof(FormatException), true), + Token = EncodedJwts.JWEInvalidHeader, + }); + + theoryData.Add(new JwtTheoryData(nameof(EncodedJwts.JWEInvalidIV)) + { + ExpectedException = new ExpectedException(typeof(ArgumentException), "IDX14309:", typeof(FormatException), true), + Token = EncodedJwts.JWEInvalidIV, + }); + + theoryData.Add(new JwtTheoryData(nameof(EncodedJwts.JWEInvalidCiphertext)) + { + ExpectedException = new ExpectedException(typeof(ArgumentException), "IDX14312:", typeof(FormatException), true), + Token = EncodedJwts.JWEInvalidCiphertext, + }); + + theoryData.Add(new JwtTheoryData(nameof(EncodedJwts.JWEInvalidAuthenticationTag)) + { + ExpectedException = new ExpectedException(typeof(ArgumentException), "IDX14311:", typeof(FormatException), true), + Token = EncodedJwts.JWEInvalidAuthenticationTag, + }); + return theoryData; } } @@ -1665,6 +1689,26 @@ public void DifferentCultureJsonWebToken() Assert.Equal("12.2", numericList[0].Value); Assert.Equal("11.1", numericList[1].Value); } + + // Test to verify equality between JsonWebTokens created from a string and an equivalent span + [Theory, MemberData(nameof(ParseTokenTheoryData))] + public void CompareJsonWebToken(JwtTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.CompareJsonWebToken", theoryData); + try + { + var tokenFromMemory = new JsonWebToken(theoryData.Token.AsMemory()); + var tokenFromString = new JsonWebToken(theoryData.Token); + + theoryData.ExpectedException.ProcessNoException(context); + IdentityComparer.AreEqual(tokenFromMemory, tokenFromString, context); + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + TestUtilities.AssertFailIfErrors(context); + } } public class ParseTimeValuesTheoryData : TheoryDataBase diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs index 2d95e766c5..0457e0f755 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs @@ -307,5 +307,25 @@ public Base64UrlEncoderTheoryData(string testId) : base(testId) { } public bool EncodingOnly { get; set; } = false; } + + [Fact] + public void ValidateAndGetOutputSizeTests() + { + string input = string.Empty; + Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize(input.AsSpan(), 0, 0)); + Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), -1, 3)); + Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), 0, -1)); + Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), 0, 4)); + Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abcde".AsSpan(), 0, 5)); + + int result = Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), 0, 0); + Assert.Equal(0, result); + + result = Base64UrlEncoding.ValidateAndGetOutputSize("abcd".AsSpan(), 0, 4); + Assert.Equal(3, result); + + result = Base64UrlEncoding.ValidateAndGetOutputSize("abc=".AsSpan(), 0, 4); + Assert.Equal(2, result); + } } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs index d415d2120e..1464e6d694 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs @@ -1229,6 +1229,7 @@ internal static void AddSignUsingSpans(byte[] bytes, SecurityKey securityKey, st }); } #endif + [Theory, MemberData(nameof(SignUsingOffsetTestCases), DisableDiscoveryEnumeration = true)] public void SignUsingOffsetTests(SignTheoryData theoryData) { diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/References.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/References.cs index eb20268271..ae1099b0e7 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/References.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/References.cs @@ -475,7 +475,11 @@ public static class EncodedJwts public static string JWEEmptyEncryptedKey = @"eyJhIjoiYiJ9..eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9"; public static string JWEEmptyIV = @"eyJhIjoiYiJ9.eyJhIjoiYiJ9..eyJhIjoiYiJ9.eyJhIjoiYiJ9"; public static string JWEEmptyCiphertext = @"eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9..eyJhIjoiYiJ9"; - public static string JWEEmptyAuthenticationTag = @"eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9."; + public static string JWEEmptyAuthenticationTag = @"eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9."; + public static string JWEInvalidHeader = @"e.eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9"; + public static string JWEInvalidIV = @"eyJhIjoiYiJ9.eyJhIjoiYiJ9.e.eyJhIjoiYiJ9.eyJhIjoiYiJ9"; + public static string JWEInvalidCiphertext = @"eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9.e.eyJhIjoiYiJ9"; + public static string JWEInvalidAuthenticationTag = @"eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9.eyJhIjoiYiJ9.e"; public static string JwsKidNullX5t { diff --git a/updateAssemblyInfo.ps1 b/updateAssemblyInfo.ps1 index ca94af04ea..10a99c3f6a 100644 --- a/updateAssemblyInfo.ps1 +++ b/updateAssemblyInfo.ps1 @@ -47,7 +47,7 @@ if ( $packageType -eq "release") } else { - $versionSuffix = $nugetSuffix + "-" + $dateTimeStamp + $versionSuffix = $nugetSuffix + "1" } Write-Host "nugetSuffix: " $nugetSuffix