diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 2c8d35c273..ec1fbdf345 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -36,6 +36,7 @@ internal static class LogMessages public const string IDX10208 = "IDX10208: Unable to validate audience. validationParameters.ValidAudience is null or whitespace and validationParameters.ValidAudiences is null."; public const string IDX10209 = "IDX10209: Token has length: '{0}' which is larger than the MaximumTokenSizeInBytes: '{1}'."; public const string IDX10211 = "IDX10211: Unable to validate issuer. The 'issuer' parameter is null or whitespace."; + public const string IDX10212 = "IDX10212: Issuer validation failed. Issuer: '{0}'. Did not match any: validationParameters.ValidIssuers: '{1}' or validationParameters.ConfigurationManager.CurrentConfiguration.Issuer: '{2}'. For more details, see https://aka.ms/IdentityModel/issuer-validation. "; public const string IDX10214 = "IDX10214: Audience validation failed. Audiences: '{0}'. Did not match: validationParameters.ValidAudience: '{1}' or validationParameters.ValidAudiences: '{2}'."; public const string IDX10215 = "IDX10215: Audience validation failed. Audiences: '{0}'. Did not match: validationParameters.ValidAudiences: '{1}'."; public const string IDX10222 = "IDX10222: Lifetime validation failed. The token is not yet valid. ValidFrom (UTC): '{0}', Current time (UTC): '{1}'."; diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs index d05f6b035c..d8c41f774d 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs @@ -21,6 +21,8 @@ internal class ValidationParameters private string _nameClaimType = ClaimsIdentity.DefaultNameClaimType; private string _roleClaimType = ClaimsIdentity.DefaultRoleClaimType; private Dictionary _instancePropertyBag; + + private IList _validIssuers; private IList _validTokenTypes; private IList _validAudiences; @@ -90,8 +92,8 @@ protected ValidationParameters(ValidationParameters other) ValidateSignatureLast = other.ValidateSignatureLast; ValidateWithLKG = other.ValidateWithLKG; ValidAlgorithms = other.ValidAlgorithms; + _validIssuers = other.ValidIssuers; _validAudiences = other.ValidAudiences; - ValidIssuers = other.ValidIssuers; _validTokenTypes = other.ValidTypes; } @@ -523,9 +525,13 @@ public TypeValidatorDelegate TypeValidator /// /// Gets the that contains valid issuers that will be used to check against the token's issuer. - /// The default is null. + /// The default is an empty collection. /// - public IList ValidIssuers { get; } + /// The that contains valid issuers that will be used to check against the token's 'iss' claim. + public IList ValidIssuers => + _validIssuers ?? + Interlocked.CompareExchange(ref _validIssuers, [], null) ?? + _validIssuers; /// /// Gets the that contains valid types that will be used to check against the JWT header's 'typ' claim. diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs index d1db65f255..602721ea21 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -15,15 +15,15 @@ namespace Microsoft.IdentityModel.Tokens /// /// The issuer to validate. /// The that is being validated. - /// The to be used for validating the token. + /// The to be used for validating the token. /// /// - /// A that contains the results of validating the issuer. + /// An that contains the results of validating the issuer. /// This delegate is not expected to throw. internal delegate Task IssuerValidationDelegateAsync( string issuer, SecurityToken securityToken, - TokenValidationParameters validationParameters, + ValidationParameters validationParameters, CallContext callContext, CancellationToken cancellationToken); @@ -37,19 +37,15 @@ public static partial class Validators /// /// The issuer to validate /// The that is being validated. - /// The to be used for validating the token. + /// The to be used for validating the token. /// /// - /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". - /// If 'validationParameters' is null. - /// If 'issuer' is null or whitespace and is true. - /// If is null or whitespace and is null. - /// If 'issuer' failed to matched either or one of . + /// An that contains the results of validating the issuer. /// An EXACT match is required. internal static async Task ValidateIssuerAsync( string issuer, SecurityToken securityToken, - TokenValidationParameters validationParameters, + ValidationParameters validationParameters, CallContext callContext, CancellationToken cancellationToken) { @@ -95,10 +91,8 @@ internal static async Task ValidateIssuerAsync( if (validationParameters.ConfigurationManager != null) configuration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(cancellationToken).ConfigureAwait(false); - // Throw if all possible places to validate against are null or empty - if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) - && validationParameters.ValidIssuers.IsNullOrEmpty() - && string.IsNullOrWhiteSpace(configuration?.Issuer)) + // Return failed IssuerValidationResult if all possible places to validate against are null or empty. + if (validationParameters.ValidIssuers.Count == 0 && string.IsNullOrWhiteSpace(configuration?.Issuer)) { return new IssuerValidationResult( issuer, @@ -126,17 +120,11 @@ internal static async Task ValidateIssuerAsync( } } - if (string.Equals(validationParameters.ValidIssuer, issuer)) + if (validationParameters.ValidIssuers.Count != 0) { - return new IssuerValidationResult(issuer, - IssuerValidationResult.ValidationSource.IssuerIsValidIssuer); - } - - if (validationParameters.ValidIssuers != null) - { - foreach (string str in validationParameters.ValidIssuers) + for (int i = 0; i < validationParameters.ValidIssuers.Count; i++) { - if (string.IsNullOrEmpty(str)) + if (string.IsNullOrEmpty(validationParameters.ValidIssuers[i])) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) LogHelper.LogInformation(LogMessages.IDX10262); @@ -144,7 +132,7 @@ internal static async Task ValidateIssuerAsync( continue; } - if (string.Equals(str, issuer)) + if (string.Equals(validationParameters.ValidIssuers[i], issuer)) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); @@ -160,9 +148,8 @@ internal static async Task ValidateIssuerAsync( ValidationFailureType.IssuerValidationFailed, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10205, + LogMessages.IDX10212, LogHelper.MarkAsNonPII(issuer), - LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), LogHelper.MarkAsNonPII(configuration?.Issuer)), typeof(SecurityTokenInvalidIssuerException), diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs index f5bacfc0b7..d96f7bc596 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs @@ -41,7 +41,7 @@ public static TheoryData AsyncIssuerValidatorTestCase theoryData.Add(new IssuerValidatorTheoryData { Issuer = null, - ValidationParameters = new TokenValidationParameters(), + ValidationParameters = new ValidationParameters(), }); return theoryData; @@ -52,7 +52,7 @@ public static TheoryData AsyncIssuerValidatorTestCase public class IssuerValidatorTheoryData : TheoryDataBase { public string Issuer { get; set; } - public TokenValidationParameters ValidationParameters { get; set; } + internal ValidationParameters ValidationParameters { get; set; } public SecurityToken SecurityToken { get; set; } } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs index 51ad101026..bd9ba6185c 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -21,29 +21,25 @@ public async Task IssuerValidatorAsyncTests(IssuerValidationResultsTheoryData th { CompareContext context = TestUtilities.WriteHeader($"{this}.IssuerValidatorAsyncTests", theoryData); - try - { - IssuerValidationResult issuerValidationResult = await Validators.ValidateIssuerAsync( - theoryData.Issuer, - theoryData.SecurityToken, - theoryData.ValidationParameters, - new CallContext(), - CancellationToken.None).ConfigureAwait(false); - - if (issuerValidationResult.Exception != null) - theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); - else - theoryData.ExpectedException.ProcessNoException(); - - IdentityComparer.AreIssuerValidationResultsEqual( - issuerValidationResult, - theoryData.IssuerValidationResult, - context); - } - catch (SecurityTokenInvalidIssuerException ex) - { - theoryData.ExpectedException.ProcessException(ex, context); - } + if (theoryData.ValidIssuerToAdd != null) + theoryData.ValidationParameters.ValidIssuers.Add(theoryData.ValidIssuerToAdd); + + IssuerValidationResult issuerValidationResult = await Validators.ValidateIssuerAsync( + theoryData.Issuer, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext(), + CancellationToken.None).ConfigureAwait(false); + + if (issuerValidationResult.Exception != null) + theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); + else + theoryData.ExpectedException.ProcessNoException(); + + IdentityComparer.AreIssuerValidationResultsEqual( + issuerValidationResult, + theoryData.IssuerValidationResult, + context); TestUtilities.AssertFailIfErrors(context); } @@ -56,26 +52,8 @@ public static TheoryData IssuerValdationResul string validIssuer = Guid.NewGuid().ToString(); string issClaim = Guid.NewGuid().ToString(); - theoryData.Add(new IssuerValidationResultsTheoryData("Invalid_Issuer") - { - ExpectedException = ExpectedException.SecurityTokenInvalidIssuerException("IDX10205:"), - Issuer = issClaim, - IssuerValidationResult = new IssuerValidationResult( - issClaim, - ValidationFailureType.IssuerValidationFailed, - new ExceptionDetail( - new MessageDetail( - LogMessages.IDX10205, - LogHelper.MarkAsNonPII(issClaim), - LogHelper.MarkAsNonPII(validIssuer), - LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), - LogHelper.MarkAsNonPII(null)), - typeof(SecurityTokenInvalidIssuerException), - new StackFrame(true))), - IsValid = false, - SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), - ValidationParameters = new TokenValidationParameters { ValidIssuer = validIssuer } - }); + var validConfig = new OpenIdConnectConfiguration() { Issuer = issClaim }; + string[] validIssuers = new string[] { validIssuer }; theoryData.Add(new IssuerValidationResultsTheoryData("NULL_Issuer") { @@ -94,7 +72,7 @@ public static TheoryData IssuerValdationResul new StackFrame(true))), IsValid = false, SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), - ValidationParameters = new TokenValidationParameters(), + ValidationParameters = new ValidationParameters() }); theoryData.Add(new IssuerValidationResultsTheoryData("NULL_ValidationParameters") @@ -132,10 +110,9 @@ public static TheoryData IssuerValdationResul null)), IsValid = false, SecurityToken = null, - ValidationParameters = new TokenValidationParameters() + ValidationParameters = new ValidationParameters() }); - var validConfig = new OpenIdConnectConfiguration() { Issuer = issClaim }; theoryData.Add(new IssuerValidationResultsTheoryData("Valid_FromConfig") { ExpectedException = ExpectedException.NoExceptionExpected, @@ -145,40 +122,44 @@ public static TheoryData IssuerValdationResul IssuerValidationResult.ValidationSource.IssuerIsConfigurationIssuer), IsValid = true, SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), - ValidationParameters = new TokenValidationParameters() + ValidationParameters = new ValidationParameters() { ConfigurationManager = new MockConfigurationManager(validConfig) } }); - theoryData.Add(new IssuerValidationResultsTheoryData("Valid_FromValidationParametersValidIssuer") + theoryData.Add(new IssuerValidationResultsTheoryData("Valid_FromValidationParametersValidIssuers") { ExpectedException = ExpectedException.NoExceptionExpected, Issuer = issClaim, IssuerValidationResult = new IssuerValidationResult( issClaim, - IssuerValidationResult.ValidationSource.IssuerIsValidIssuer), + IssuerValidationResult.ValidationSource.IssuerIsAmongValidIssuers), IsValid = true, SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), - ValidationParameters = new TokenValidationParameters() - { - ValidIssuer = issClaim - } + ValidationParameters = new ValidationParameters(), + ValidIssuerToAdd = issClaim }); - theoryData.Add(new IssuerValidationResultsTheoryData("Valid_FromValidationParametersValidIssuers") + theoryData.Add(new IssuerValidationResultsTheoryData("Invalid_Issuer") { - ExpectedException = ExpectedException.NoExceptionExpected, + ExpectedException = ExpectedException.SecurityTokenInvalidIssuerException("IDX10212:"), Issuer = issClaim, IssuerValidationResult = new IssuerValidationResult( issClaim, - IssuerValidationResult.ValidationSource.IssuerIsAmongValidIssuers), - IsValid = true, + ValidationFailureType.IssuerValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10212, + LogHelper.MarkAsNonPII(issClaim), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validIssuers)), + LogHelper.MarkAsNonPII(null)), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))), + IsValid = false, SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), - ValidationParameters = new TokenValidationParameters() - { - ValidIssuers = [issClaim] - } + ValidationParameters = new ValidationParameters(), + ValidIssuerToAdd = validIssuer }); return theoryData; @@ -202,8 +183,9 @@ public IssuerValidationResultsTheoryData(string testId) : base(testId) public SecurityToken SecurityToken { get; set; } - public TokenValidationParameters ValidationParameters { get; set; } + internal ValidationParameters ValidationParameters { get; set; } internal ValidationFailureType ValidationFailureType { get; set; } + public string ValidIssuerToAdd { get; internal set; } } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidationParametersTests.cs index a3cdce765b..32ace71acd 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidationParametersTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -20,6 +20,14 @@ public void SetValidators_NullValue_ThrowsArgumentNullException() Assert.Throws(() => validationParameters.AudienceValidator = null); } + [Fact] + public void ValidIssuers_GetReturnsEmptyList() + { + var validationParameters = new ValidationParameters(); + + Assert.Equal(0, validationParameters.ValidIssuers.Count); + } + [Fact] public void ValidAudiences_Get_ReturnsEmptyList() { @@ -33,6 +41,7 @@ public void ValidAudiences_Get_ReturnsEmptyList() public void ValidTypes_Get_ReturnsEmptyList() { var validationParameters = new ValidationParameters(); + Assert.Equal(0, validationParameters.ValidTypes.Count); Assert.True(validationParameters.ValidTypes is IList); }