From 74703533fea3c20cc290bddff355333c2a43e28f Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Tue, 14 Feb 2023 16:27:30 +0100 Subject: [PATCH] Ensure backchannel is used with the IdTokenValidator (#245) --- src/Auth0.OidcClient.Core/Auth0ClientBase.cs | 6 ++- .../Tokens/AsymmetricSignatureVerifier.cs | 13 +++++-- .../Tokens/IdTokenValidator.cs | 14 +++++-- .../Tokens/JsonWebKeys.cs | 16 ++++++-- .../Auth0.OidcClient.Core.UnitTests.csproj | 4 ++ .../Tokens/IdTokenValidatorTests.cs | 37 ++++++++++++++++++- 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/Auth0.OidcClient.Core/Auth0ClientBase.cs b/src/Auth0.OidcClient.Core/Auth0ClientBase.cs index bcafe20b..477864dd 100644 --- a/src/Auth0.OidcClient.Core/Auth0ClientBase.cs +++ b/src/Auth0.OidcClient.Core/Auth0ClientBase.cs @@ -22,6 +22,7 @@ namespace Auth0.OidcClient public abstract class Auth0ClientBase : IAuth0Client { private readonly IdTokenRequirements _idTokenRequirements; + private readonly IdTokenValidator _idTokenValidator; private readonly Auth0ClientOptions _options; private readonly string _userAgent; private IdentityModel.OidcClient.OidcClient _oidcClient; @@ -43,6 +44,7 @@ protected Auth0ClientBase(Auth0ClientOptions options, string platformName) _options = options; _idTokenRequirements = new IdTokenRequirements($"https://{_options.Domain}/", _options.ClientId, options.Leeway, options.MaxAge); _userAgent = CreateAgentString(platformName); + _idTokenValidator = new IdTokenValidator(options.BackchannelHandler); } /// @@ -65,7 +67,7 @@ public async Task LoginAsync(object extraParameters = null, Cancell _idTokenRequirements.Organization = finalExtraParameters["organization"]; } - await IdTokenValidator.AssertTokenMeetsRequirements(_idTokenRequirements, result.IdentityToken); // Nonce is created & tested by OidcClient + await _idTokenValidator.AssertTokenMeetsRequirements(_idTokenRequirements, result.IdentityToken); // Nonce is created & tested by OidcClient } return result; @@ -121,7 +123,7 @@ public async Task RefreshTokenAsync(string refreshToken, obj _idTokenRequirements.Organization = finalExtraParameters["Organization"]; } - await IdTokenValidator.AssertTokenMeetsRequirements(_idTokenRequirements, result.IdentityToken); // Nonce is created & tested by OidcClient + await _idTokenValidator.AssertTokenMeetsRequirements(_idTokenRequirements, result.IdentityToken); // Nonce is created & tested by OidcClient } return result; diff --git a/src/Auth0.OidcClient.Core/Tokens/AsymmetricSignatureVerifier.cs b/src/Auth0.OidcClient.Core/Tokens/AsymmetricSignatureVerifier.cs index c88d3429..04df45ec 100644 --- a/src/Auth0.OidcClient.Core/Tokens/AsymmetricSignatureVerifier.cs +++ b/src/Auth0.OidcClient.Core/Tokens/AsymmetricSignatureVerifier.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; using System.Threading.Tasks; namespace Auth0.OidcClient.Tokens @@ -9,11 +10,17 @@ namespace Auth0.OidcClient.Tokens internal class AsymmetricSignatureVerifier : ISignatureVerifier { private readonly IList keys; + private readonly JsonWebKeys jsonWebKeys; - public static async Task ForJwks(string issuer) + public AsymmetricSignatureVerifier(HttpMessageHandler backchannel = null) { - var jsonWebKeys = await JsonWebKeys.GetForIssuer(issuer); - return new AsymmetricSignatureVerifier(jsonWebKeys.Keys); + jsonWebKeys = new JsonWebKeys(backchannel); + } + + public async Task ForJwks(string issuer) + { + var jsonWebKeysSet = await jsonWebKeys.GetForIssuer(issuer); + return new AsymmetricSignatureVerifier(jsonWebKeysSet.Keys); } public AsymmetricSignatureVerifier(IList keys) diff --git a/src/Auth0.OidcClient.Core/Tokens/IdTokenValidator.cs b/src/Auth0.OidcClient.Core/Tokens/IdTokenValidator.cs index 0a2fae78..72eebd05 100644 --- a/src/Auth0.OidcClient.Core/Tokens/IdTokenValidator.cs +++ b/src/Auth0.OidcClient.Core/Tokens/IdTokenValidator.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; +using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; @@ -12,8 +13,15 @@ namespace Auth0.OidcClient.Tokens /// /// Perform validation of a JWT ID token in compliance with specified . /// - internal static class IdTokenValidator + internal class IdTokenValidator { + private readonly AsymmetricSignatureVerifier assymetricSignatureVerifier; + + public IdTokenValidator(HttpMessageHandler backchannel = null) + { + assymetricSignatureVerifier = new AsymmetricSignatureVerifier(backchannel); + } + /// /// Assert that all the are met by a JWT ID token for a given point in time. /// @@ -26,7 +34,7 @@ internal static class IdTokenValidator /// meet the requirements specified by . /// /// that will complete when the token is validated. - internal static async Task AssertTokenMeetsRequirements(this IdTokenRequirements required, string rawIDToken, DateTime? pointInTime = null, ISignatureVerifier signatureVerifier = null) + internal async Task AssertTokenMeetsRequirements(IdTokenRequirements required, string rawIDToken, DateTime? pointInTime = null, ISignatureVerifier signatureVerifier = null) { if (string.IsNullOrWhiteSpace(rawIDToken)) throw new IdTokenValidationException("ID token is required but missing."); @@ -36,7 +44,7 @@ internal static async Task AssertTokenMeetsRequirements(this IdTokenRequirements // For now we want to support HS256 + ClientSecret as we just had a major release. // TODO: In the next major (v4.0) we should remove this condition as well as Auth0ClientOptions.ClientSecret if (token.SignatureAlgorithm != "HS256") - (signatureVerifier ?? await AsymmetricSignatureVerifier.ForJwks(required.Issuer)).VerifySignature(rawIDToken); + (signatureVerifier ?? await assymetricSignatureVerifier.ForJwks(required.Issuer)).VerifySignature(rawIDToken); AssertTokenClaimsMeetRequirements(required, token, pointInTime ?? DateTime.Now); } diff --git a/src/Auth0.OidcClient.Core/Tokens/JsonWebKeys.cs b/src/Auth0.OidcClient.Core/Tokens/JsonWebKeys.cs index 3dc7d531..3d13accb 100644 --- a/src/Auth0.OidcClient.Core/Tokens/JsonWebKeys.cs +++ b/src/Auth0.OidcClient.Core/Tokens/JsonWebKeys.cs @@ -2,24 +2,32 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using System; +using System.Net.Http; using System.Threading.Tasks; namespace Auth0.OidcClient.Tokens { - static class JsonWebKeys + class JsonWebKeys { - public static async Task GetForIssuer(string issuer) + private readonly HttpMessageHandler backchannel; + + public JsonWebKeys(HttpMessageHandler backchannel = null) + { + this.backchannel = backchannel; + } + + public async Task GetForIssuer(string issuer) { var metadataAddress = new UriBuilder(issuer) { Path = "/.well-known/openid-configuration" }.Uri.OriginalString; var openIdConfiguration = await GetOpenIdConfiguration(metadataAddress); return openIdConfiguration.JsonWebKeySet; } - private static Task GetOpenIdConfiguration(string metadataAddress) + private Task GetOpenIdConfiguration(string metadataAddress) { try { - var configurationManager = new ConfigurationManager(metadataAddress, new OpenIdConnectConfigurationRetriever()); + var configurationManager = backchannel == null ? new ConfigurationManager(metadataAddress, new OpenIdConnectConfigurationRetriever()) : new ConfigurationManager(metadataAddress, new OpenIdConnectConfigurationRetriever(), new System.Net.Http.HttpClient(backchannel)); return configurationManager.GetConfigurationAsync(); } catch (Exception e) diff --git a/test/Auth0.OidcClient.Core.UnitTests/Auth0.OidcClient.Core.UnitTests.csproj b/test/Auth0.OidcClient.Core.UnitTests/Auth0.OidcClient.Core.UnitTests.csproj index 043b7bf8..6ae9f8df 100644 --- a/test/Auth0.OidcClient.Core.UnitTests/Auth0.OidcClient.Core.UnitTests.csproj +++ b/test/Auth0.OidcClient.Core.UnitTests/Auth0.OidcClient.Core.UnitTests.csproj @@ -14,8 +14,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/test/Auth0.OidcClient.Core.UnitTests/Tokens/IdTokenValidatorTests.cs b/test/Auth0.OidcClient.Core.UnitTests/Tokens/IdTokenValidatorTests.cs index 33faf2fa..86b2a5d0 100644 --- a/test/Auth0.OidcClient.Core.UnitTests/Tokens/IdTokenValidatorTests.cs +++ b/test/Auth0.OidcClient.Core.UnitTests/Tokens/IdTokenValidatorTests.cs @@ -1,6 +1,13 @@ using Auth0.OidcClient.Tokens; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; +using Moq; +using Moq.Protected; using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -28,7 +35,7 @@ public class IdTokenValidatorUnitTests private Task ValidateToken(string token, IdTokenRequirements reqs = null, DateTime? when = null, ISignatureVerifier signatureVerifier = null) { - return IdTokenValidator.AssertTokenMeetsRequirements(reqs ?? defaultReqs, token, when ?? tokensWereValid, signatureVerifier ?? rs256NoSignature); + return new OidcClient.Tokens.IdTokenValidator().AssertTokenMeetsRequirements(reqs ?? defaultReqs, token, when ?? tokensWereValid, signatureVerifier ?? rs256NoSignature); } [Fact] @@ -294,5 +301,33 @@ public async void DoesNotThrowWhenOrgIdAvailableButNotARequirement() await ValidateToken(token); } + + [Fact] + public async void UsesTheBackchannelWhenProvidedToFetchTheOpenIdConfiguration() + { + var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1qQXlNamczTWpFMVF6WXhNamhGUkVKR09FRkVSRGMzTlRoRU9EWTNRak13UVRSR056UkdRUSJ9.eyJuaWNrbmFtZSI6ImRhbWllbmcrdGVzdDQyIiwibmFtZSI6ImRhbWllbmcrdGVzdDQyQGdtYWlsLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MzFiMDJkYTllOWVjNzg3ZDBlMWE1NzA1YzQ0YzU2Nj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRmRhLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDE5LTExLTAxVDE3OjQ0OjE2LjY5NVoiLCJlbWFpbCI6ImRhbWllbmcrdGVzdDQyQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiaXNzIjoiaHR0cHM6Ly9hdXRoMC1kb3RuZXQtaW50ZWdyYXRpb24tdGVzdHMuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDVkYTY0NTNjMTIyZmI2MGE5MjRlOTI2MSIsImF1ZCI6InFtc3M5QTY2c3RQV1RPWGpSNlgxT2VBMERMYWRvTlAyIiwiaWF0IjoxNTcyNjMwMjU2LCJleHAiOjE1NzI2NjYyNTYsIm5vbmNlIjoiU09ySE9hdTlxMGl0eDRpVEZfaVgydyJ9.NomT02whkH42ISpcd_JvG4ZvQQhzPKfoWCwcgrhyLeWmnmHTo704WtsnfCqR72uw26D-ZGA5n2Yu4Jdcv2A8_leGEQm3p45-ramIDwWUu2J30m_op_5I4wFvgpbRrWSrD1_3qK1GrDnrdv8psGL8VgCf3pLLDbqbkzDmtE6OtEfDp2hEFwXs9YntREXu5Z-ufFFLz9VU5uyRg7JA95YGQNIRhzMFoUNKZAO19nrBq3HKc_iR_W9g9Y3iLPLgVVazq6zHjn3cXNKpr7JN6MUKqIB-YYJ1KDEvmaMO60xs2DAhhnkUN1OhXBLTgQ9xbCJeaxE7N48YMxPAu3HHT-rhZg"; + + var mockHandler = new Mock(MockBehavior.Strict); + mockHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(req => req.RequestUri.ToString() == $"https://auth0-dotnet-integration-tests.auth0.com/.well-known/openid-configuration"), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(new OpenIdConnectConfiguration() { JsonWebKeySet = signingKeys }), Encoding.UTF8, "application/json"), + }); + + await new OidcClient.Tokens.IdTokenValidator(mockHandler.Object).AssertTokenMeetsRequirements(signedReqs, token, new DateTime(2019, 11, 1, 20, 00, 00, DateTimeKind.Utc)); + + + mockHandler.Protected().Verify( + "SendAsync", + Times.Exactly(1), + ItExpr.Is(req => req.RequestUri.ToString() == $"https://auth0-dotnet-integration-tests.auth0.com/.well-known/openid-configuration"), + ItExpr.IsAny()); + } } }