From c084658438e3ab12119cd3599cf162e4a7f6c177 Mon Sep 17 00:00:00 2001 From: Owen Smith Date: Thu, 1 Aug 2024 16:02:21 -0400 Subject: [PATCH] update to Microsoft.IdentityModel.JsonWebTokens --- Directory.Packages.props | 2 +- .../D2L.Security.OAuth2.csproj | 2 +- .../Keys/Default/TokenSigner.cs | 32 ++++++------ .../Default/CachedAccessTokenProvider.cs | 10 +--- .../Validation/AccessTokens/AccessToken.cs | 13 ++--- .../AccessTokens/AccessTokenValidator.cs | 50 +++++++++---------- ...2L.Security.OAuth2.IntegrationTests.csproj | 2 +- .../D2L.Security.OAuth2.UnitTests.csproj | 2 +- .../PrivateKeyProviderTests.Concurrency.cs | 26 +++++----- .../Default/AccessTokenProviderTests.cs | 12 ++--- .../Validation/AccessTokenValidatorTests.cs | 16 +++--- 11 files changed, 78 insertions(+), 89 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 04e0a111..4d523006 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + diff --git a/src/D2L.Security.OAuth2/D2L.Security.OAuth2.csproj b/src/D2L.Security.OAuth2/D2L.Security.OAuth2.csproj index 914a770d..fd36da48 100644 --- a/src/D2L.Security.OAuth2/D2L.Security.OAuth2.csproj +++ b/src/D2L.Security.OAuth2/D2L.Security.OAuth2.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/D2L.Security.OAuth2/Keys/Default/TokenSigner.cs b/src/D2L.Security.OAuth2/Keys/Default/TokenSigner.cs index 8bb0fd64..b837870d 100644 --- a/src/D2L.Security.OAuth2/Keys/Default/TokenSigner.cs +++ b/src/D2L.Security.OAuth2/Keys/Default/TokenSigner.cs @@ -1,14 +1,15 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Security.Claims; +using Microsoft.IdentityModel.JsonWebTokens; using System.Threading.Tasks; using D2L.CodeStyle.Annotations; using D2L.Security.OAuth2.Validation.Exceptions; +using Microsoft.IdentityModel.Tokens; +using System.Collections.Generic; namespace D2L.Security.OAuth2.Keys.Default { public sealed partial class TokenSigner : ITokenSigner { private readonly IPrivateKeyProvider m_privateKeyProvider; + private readonly JsonWebTokenHandler m_tokenHandler = new() { SetDefaultTimesOnTokenCreation = false }; public TokenSigner( IKeyManagementService keyManagementService @@ -22,31 +23,28 @@ IPrivateKeyProvider privateKeyProvider [GenerateSync] async Task ITokenSigner.SignAsync( UnsignedToken token ) { - JwtSecurityToken jwt; using( D2LSecurityToken securityToken = await m_privateKeyProvider .GetSigningCredentialsAsync() .ConfigureAwait( false ) ) { - jwt = new JwtSecurityToken( - issuer: token.Issuer, - audience: token.Audience, - claims: Enumerable.Empty(), - notBefore: token.NotBefore, - expires: token.ExpiresAt, - signingCredentials: securityToken.GetSigningCredentials() - ); + SecurityTokenDescriptor jwt = new SecurityTokenDescriptor() { + Issuer = token.Issuer, + Audience = token.Audience, + NotBefore = token.NotBefore, + Expires = token.ExpiresAt, + SigningCredentials = securityToken.GetSigningCredentials(), + Claims = new Dictionary(), + }; var claims = token.Claims; foreach( var claim in claims ) { - if( jwt.Payload.ContainsKey( claim.Key ) ) { + if( jwt.Claims.ContainsKey( claim.Key ) ) { throw new ValidationException( $"'{claim.Key}' is already part of the payload" ); } - jwt.Payload.Add( claim.Key, claim.Value ); + jwt.Claims.Add( claim.Key, claim.Value ); } - var jwtHandler = new JwtSecurityTokenHandler(); - - string signedRawToken = jwtHandler.WriteToken( jwt ); + string signedRawToken = m_tokenHandler.CreateToken( jwt ); return signedRawToken; } diff --git a/src/D2L.Security.OAuth2/Provisioning/Default/CachedAccessTokenProvider.cs b/src/D2L.Security.OAuth2/Provisioning/Default/CachedAccessTokenProvider.cs index f579d624..3ca9cd4e 100644 --- a/src/D2L.Security.OAuth2/Provisioning/Default/CachedAccessTokenProvider.cs +++ b/src/D2L.Security.OAuth2/Provisioning/Default/CachedAccessTokenProvider.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.JsonWebTokens; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -10,16 +10,12 @@ using D2L.Services; using D2L.CodeStyle.Annotations; -#if DNXCORE50 -using System.IdentityModel.Tokens.Jwt; -#endif - namespace D2L.Security.OAuth2.Provisioning.Default { internal sealed partial class CachedAccessTokenProvider : IAccessTokenProvider { private readonly INonCachingAccessTokenProvider m_accessTokenProvider; private readonly Uri m_authEndpoint; private readonly TimeSpan m_tokenRefreshGracePeriod; - private readonly JwtSecurityTokenHandler m_tokenHandler; + private readonly JsonWebTokenHandler m_tokenHandler = new(); public CachedAccessTokenProvider( INonCachingAccessTokenProvider accessTokenProvider, @@ -29,8 +25,6 @@ TimeSpan tokenRefreshGracePeriod m_accessTokenProvider = accessTokenProvider; m_authEndpoint = authEndpoint; m_tokenRefreshGracePeriod = tokenRefreshGracePeriod; - - m_tokenHandler = new JwtSecurityTokenHandler(); } [GenerateSync] diff --git a/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessToken.cs b/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessToken.cs index 6c05b295..03fc2dac 100644 --- a/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessToken.cs +++ b/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessToken.cs @@ -1,23 +1,18 @@ using System; using System.Collections.Generic; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.JsonWebTokens; using System.Security.Claims; using D2L.CodeStyle.Annotations; using static D2L.CodeStyle.Annotations.Objects; -#if DNXCORE50 -using System.IdentityModel.Tokens.Jwt; -#endif - namespace D2L.Security.OAuth2.Validation.AccessTokens { [Immutable] internal sealed class AccessToken : IAccessToken { [Mutability.Audited( "Todd Lang", "02-Mar-2018", ".Net class we can't modify, but is used immutably." )] - private readonly JwtSecurityToken m_inner; + private readonly JsonWebToken m_inner; private readonly IAccessToken m_this; - internal AccessToken( JwtSecurityToken jwtSecurityToken ) { + internal AccessToken( JsonWebToken jwtSecurityToken ) { m_inner = jwtSecurityToken; m_this = this; } @@ -35,7 +30,7 @@ IEnumerable IAccessToken.Claims { } string IAccessToken.SensitiveRawAccessToken { - get { return m_inner.RawData; } + get { return m_inner.EncodedToken; } } } } diff --git a/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessTokenValidator.cs b/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessTokenValidator.cs index dcdf26f5..f1d6964f 100644 --- a/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessTokenValidator.cs +++ b/src/D2L.Security.OAuth2/Validation/AccessTokens/AccessTokenValidator.cs @@ -1,19 +1,24 @@ using System; using System.Collections.Immutable; using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Threading; +using Microsoft.IdentityModel.JsonWebTokens; using System.Threading.Tasks; using D2L.Security.OAuth2.Keys.Default; using D2L.Security.OAuth2.Validation.Exceptions; using D2L.Services; using D2L.CodeStyle.Annotations; -#if DNXCORE50 -using System.IdentityModel.Tokens.Jwt; -#endif - namespace D2L.Security.OAuth2.Validation.AccessTokens { + internal static class JsonWebTokenHandlerExtensions { + + public static TokenValidationResult ValidateToken( + this JsonWebTokenHandler @this, + SecurityToken token, + TokenValidationParameters validationParameters + ) => @this.ValidateTokenAsync( token, validationParameters ).ConfigureAwait( false ).GetAwaiter().GetResult(); + + } + internal sealed partial class AccessTokenValidator : IAccessTokenValidator { internal static readonly ImmutableHashSet ALLOWED_SIGNATURE_ALGORITHMS = ImmutableHashSet.Create( SecurityAlgorithms.RsaSha256, @@ -23,10 +28,7 @@ internal sealed partial class AccessTokenValidator : IAccessTokenValidator { ); private readonly IPublicKeyProvider m_publicKeyProvider; - private readonly ThreadLocal m_tokenHandler = new ThreadLocal( - valueFactory: () => new JwtSecurityTokenHandler(), - trackAllValues: false - ); + private readonly JsonWebTokenHandler m_tokenHandler = new(); public AccessTokenValidator( IPublicKeyProvider publicKeyProvider @@ -41,31 +43,27 @@ IPublicKeyProvider publicKeyProvider async Task IAccessTokenValidator.ValidateAsync( string token ) { - var tokenHandler = m_tokenHandler.Value; - - if( !tokenHandler.CanReadToken( token ) ) { + if( !m_tokenHandler.CanReadToken( token ) ) { throw new ValidationException( "Couldn't parse token" ); } - var unvalidatedToken = ( JwtSecurityToken )tokenHandler.ReadToken( + var unvalidatedToken = ( JsonWebToken )m_tokenHandler.ReadToken( token ); - if( !ALLOWED_SIGNATURE_ALGORITHMS.Contains( unvalidatedToken.SignatureAlgorithm ) ) { + if( !ALLOWED_SIGNATURE_ALGORITHMS.Contains( unvalidatedToken.Alg ) ) { string message = string.Format( "Signature algorithm '{0}' is not supported. Permitted algorithms are '{1}'", - unvalidatedToken.SignatureAlgorithm, + unvalidatedToken.Alg, string.Join( ",", ALLOWED_SIGNATURE_ALGORITHMS ) ); throw new InvalidTokenException( message ); } - if( !unvalidatedToken.Header.ContainsKey( "kid" ) ) { + if( !unvalidatedToken.TryGetHeaderValue( "kid", out string keyId ) ) { throw new InvalidTokenException( "KeyId not found in token" ); } - string keyId = unvalidatedToken.Header[ "kid" ].ToString(); - using D2LSecurityToken signingKey = ( await m_publicKeyProvider .GetByIdAsync( keyId ) .ConfigureAwait( false ) @@ -82,12 +80,14 @@ string token IAccessToken accessToken; try { - tokenHandler.ValidateToken( - token, - validationParameters, - out SecurityToken securityToken - ); - accessToken = new AccessToken( ( JwtSecurityToken )securityToken ); + TokenValidationResult validationResult = await m_tokenHandler.ValidateTokenAsync( + unvalidatedToken, + validationParameters + ).ConfigureAwait( false ); + if( !validationResult.IsValid ) { + throw validationResult.Exception; + } + accessToken = new AccessToken( (JsonWebToken)validationResult.SecurityToken ); } catch( SecurityTokenExpiredException e ) { throw new ExpiredTokenException( e ); } catch( SecurityTokenNotYetValidException e ) { diff --git a/test/D2L.Security.OAuth2.IntegrationTests/D2L.Security.OAuth2.IntegrationTests.csproj b/test/D2L.Security.OAuth2.IntegrationTests/D2L.Security.OAuth2.IntegrationTests.csproj index 9f04e205..5bd67272 100644 --- a/test/D2L.Security.OAuth2.IntegrationTests/D2L.Security.OAuth2.IntegrationTests.csproj +++ b/test/D2L.Security.OAuth2.IntegrationTests/D2L.Security.OAuth2.IntegrationTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/D2L.Security.OAuth2.UnitTests/D2L.Security.OAuth2.UnitTests.csproj b/test/D2L.Security.OAuth2.UnitTests/D2L.Security.OAuth2.UnitTests.csproj index bc940622..3f43dc20 100644 --- a/test/D2L.Security.OAuth2.UnitTests/D2L.Security.OAuth2.UnitTests.csproj +++ b/test/D2L.Security.OAuth2.UnitTests/D2L.Security.OAuth2.UnitTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/test/D2L.Security.OAuth2.UnitTests/Keys/Default/PrivateKeyProviderTests.Concurrency.cs b/test/D2L.Security.OAuth2.UnitTests/Keys/Default/PrivateKeyProviderTests.Concurrency.cs index 04ebf786..d43b6b6c 100644 --- a/test/D2L.Security.OAuth2.UnitTests/Keys/Default/PrivateKeyProviderTests.Concurrency.cs +++ b/test/D2L.Security.OAuth2.UnitTests/Keys/Default/PrivateKeyProviderTests.Concurrency.cs @@ -5,7 +5,7 @@ using System.Threading; using D2L.Services; using NUnit.Framework; -using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.JsonWebTokens; namespace D2L.Security.OAuth2.Keys.Default { [TestFixture] @@ -82,13 +82,13 @@ int threadNumber } private static string Sign( D2LSecurityToken securityToken ) { - JwtSecurityToken jwt = new JwtSecurityToken( - issuer: TEST_ISSUER, - signingCredentials: securityToken.GetSigningCredentials() - ); + SecurityTokenDescriptor jwt = new() { + Issuer = TEST_ISSUER, + SigningCredentials = securityToken.GetSigningCredentials() + }; - JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler(); - string signedToken = jwtHandler.WriteToken( jwt ); + JsonWebTokenHandler jwtHandler = new(); + string signedToken = jwtHandler.CreateToken( jwt ); return signedToken; } @@ -97,7 +97,7 @@ private static void AssertSignatureVerifiable( D2LSecurityToken securityToken, string signedToken ) { - JwtSecurityTokenHandler validationTokenHandler = new JwtSecurityTokenHandler(); + JsonWebTokenHandler validationTokenHandler = new(); TokenValidationParameters validationParameters = new TokenValidationParameters() { ValidateAudience = false, ValidateIssuer = false, @@ -105,13 +105,15 @@ string signedToken RequireSignedTokens = true, IssuerSigningKey = securityToken }; - validationTokenHandler.ValidateToken( + TokenValidationResult validationResult = validationTokenHandler.ValidateToken( signedToken, - validationParameters, - out SecurityToken validatedToken + validationParameters ); + if( !validationResult.IsValid ) { + throw validationResult.Exception; + } - JwtSecurityToken validatedJwt = validatedToken as JwtSecurityToken; + JsonWebToken validatedJwt = validationResult.SecurityToken as JsonWebToken; Assert.AreEqual( TEST_ISSUER, validatedJwt.Issuer ); } } diff --git a/test/D2L.Security.OAuth2.UnitTests/Provisioning/Default/AccessTokenProviderTests.cs b/test/D2L.Security.OAuth2.UnitTests/Provisioning/Default/AccessTokenProviderTests.cs index 89e38962..60ae7b77 100644 --- a/test/D2L.Security.OAuth2.UnitTests/Provisioning/Default/AccessTokenProviderTests.cs +++ b/test/D2L.Security.OAuth2.UnitTests/Provisioning/Default/AccessTokenProviderTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.JsonWebTokens; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -25,7 +25,7 @@ private static class TestData { private IPublicKeyDataProvider m_publicKeyDataProvider; private ITokenSigner m_tokenSigner; private INonCachingAccessTokenProvider m_accessTokenProvider; - private JwtSecurityToken m_actualAssertion; + private JsonWebToken m_actualAssertion; [SetUp] public void SetUp() { @@ -33,8 +33,8 @@ public void SetUp() { clientMock .Setup( x => x.ProvisionAccessTokenAsync( It.IsAny(), It.IsAny>() ) ) .Callback>( ( assertion, _ ) => { - var tokenHandler = new JwtSecurityTokenHandler(); - m_actualAssertion = ( JwtSecurityToken )tokenHandler.ReadToken( assertion ); + var tokenHandler = new JsonWebTokenHandler(); + m_actualAssertion = ( JsonWebToken )tokenHandler.ReadToken( assertion ); } ) .ReturnsAsync( value: null ); @@ -63,7 +63,7 @@ await m_accessTokenProvider var publicKeys = ( await m_publicKeyDataProvider.GetAllAsync().ConfigureAwait( false ) ).ToList(); string expectedKeyId = publicKeys.First().Id.ToString(); - string actualKeyId = m_actualAssertion.Header.Kid; + string actualKeyId = m_actualAssertion.GetHeaderValue( "kid" ); Assert.AreEqual( 1, publicKeys.Count ); Assert.AreEqual( expectedKeyId, actualKeyId ); @@ -90,7 +90,7 @@ await m_accessTokenProvider AssertClaimEquals( m_actualAssertion, Constants.Claims.USER_ID, TestData.USER ); } - private void AssertClaimEquals( JwtSecurityToken token, string name, string value ) { + private void AssertClaimEquals( JsonWebToken token, string name, string value ) { Claim claim = token.Claims.FirstOrDefault( c => c.Type == name ); Assert.IsNotNull( claim ); Assert.AreEqual( value, claim.Value ); diff --git a/test/D2L.Security.OAuth2.UnitTests/Validation/AccessTokenValidatorTests.cs b/test/D2L.Security.OAuth2.UnitTests/Validation/AccessTokenValidatorTests.cs index e906a707..9c5a9642 100644 --- a/test/D2L.Security.OAuth2.UnitTests/Validation/AccessTokenValidatorTests.cs +++ b/test/D2L.Security.OAuth2.UnitTests/Validation/AccessTokenValidatorTests.cs @@ -1,6 +1,6 @@ using System; using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.JsonWebTokens; using System.Threading.Tasks; using D2L.Security.OAuth2.Keys.Default; using D2L.Security.OAuth2.TestUtilities; @@ -83,14 +83,14 @@ private async Task RunTest( signingCredentials = signingToken.GetSigningCredentials(); } - var jwtToken = new JwtSecurityToken( - issuer: "someissuer", - signingCredentials: signingCredentials, - expires: jwtExpiry - ); + SecurityTokenDescriptor jwtToken = new() { + Issuer = "someissuer", + SigningCredentials = signingCredentials, + Expires = jwtExpiry + }; - var tokenHandler = new JwtSecurityTokenHandler(); - string serializedJwt = tokenHandler.WriteToken( jwtToken ); + JsonWebTokenHandler tokenHandler = new() { SetDefaultTimesOnTokenCreation = false }; + string serializedJwt = tokenHandler.CreateToken( jwtToken ); IPublicKeyProvider publicKeyProvider = PublicKeyProviderMock.Create( m_jwksEndpoint,